KAI MAURIN-JONES
commited on
Commit
β’
6a1c250
1
Parent(s):
fea2e56
files added
Browse files- .DS_Store +0 -0
- LICENSE +21 -0
- README.md +3 -12
- __pycache__/wordle_functions.cpython-310.pyc +0 -0
- app.py +151 -0
- data/official_words_processed.txt +2309 -0
- notebooks/__pycache__/wordle_assistant_functions.cpython-310.pyc +0 -0
- notebooks/__pycache__/wordle_functions.cpython-310.pyc +0 -0
- notebooks/getting_daily_word.ipynb +112 -0
- notebooks/wordle_assistant.ipynb +1392 -0
- notebooks/wordle_assistant_functions.py +1350 -0
- plots.py +86 -0
- requirements.txt +5 -0
- wordle_assistant_functions.py +1347 -0
.DS_Store
ADDED
Binary file (6.15 kB). View file
|
|
LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
MIT License
|
2 |
+
|
3 |
+
Copyright (c) 2023 Kai Maurin-Jones
|
4 |
+
|
5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6 |
+
of this software and associated documentation files (the "Software"), to deal
|
7 |
+
in the Software without restriction, including without limitation the rights
|
8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9 |
+
copies of the Software, and to permit persons to whom the Software is
|
10 |
+
furnished to do so, subject to the following conditions:
|
11 |
+
|
12 |
+
The above copyright notice and this permission notice shall be included in all
|
13 |
+
copies or substantial portions of the Software.
|
14 |
+
|
15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21 |
+
SOFTWARE.
|
README.md
CHANGED
@@ -1,13 +1,4 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
emoji: π»
|
4 |
-
colorFrom: gray
|
5 |
-
colorTo: indigo
|
6 |
-
sdk: streamlit
|
7 |
-
sdk_version: 1.21.0
|
8 |
-
app_file: app.py
|
9 |
-
pinned: false
|
10 |
-
license: mit
|
11 |
-
---
|
12 |
|
13 |
-
|
|
|
1 |
+
# Wordle Wizard Assistant
|
2 |
+
Streamlit Cloud web app deployment of a derivation of the Wordle Wizard backend. See [Wordle Wizard repo](https://github.com/kmaurinjones/wordle_wizard) for more details of original solving algorithm and development process. This app and version of the algorithm are modified to a) scrape the daily Wordle solution ("target") word, and b) take user input as guesses for each word, and provide helpful hints as to the most statistically optimal next word for the given puzzle.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3 |
|
4 |
+
If you have any questions about this app or would like to request any new features, please contact me at my email address at kmaurinjones@gmail.com
|
__pycache__/wordle_functions.cpython-310.pyc
ADDED
Binary file (30 kB). View file
|
|
app.py
ADDED
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
from streamlit_extras.stateful_button import button # for button that can maintain its clicked state
|
3 |
+
import random # for showing random words
|
4 |
+
from wordle_assistant_functions import * # for wordle solving
|
5 |
+
import plotly.express as px # for plots
|
6 |
+
from plots import * # for plots
|
7 |
+
|
8 |
+
### for getting daily target word
|
9 |
+
from bs4 import BeautifulSoup
|
10 |
+
|
11 |
+
# !pip install selenium
|
12 |
+
from selenium import webdriver
|
13 |
+
from selenium.webdriver.common.keys import Keys
|
14 |
+
from selenium.webdriver.chrome.options import Options
|
15 |
+
|
16 |
+
# Configure ChromeOptions
|
17 |
+
chrome_options = Options()
|
18 |
+
chrome_options.add_argument("--headless") # Run Chrome in headless mode
|
19 |
+
driver = webdriver.Chrome(options = chrome_options)
|
20 |
+
|
21 |
+
url = "https://screenrant.com/wordle-answers-updated-word-puzzle-guide/"
|
22 |
+
|
23 |
+
# Navigate to the URL
|
24 |
+
driver.get(url)
|
25 |
+
|
26 |
+
# Get the page source
|
27 |
+
html_content = driver.page_source
|
28 |
+
|
29 |
+
soup = BeautifulSoup(html_content, "html.parser")
|
30 |
+
for item in soup.find_all('a'):
|
31 |
+
|
32 |
+
item_link = item['href']
|
33 |
+
|
34 |
+
link_prefix = "https://screenrant.com/todays-wordle-answer-hints"
|
35 |
+
|
36 |
+
if item_link:
|
37 |
+
|
38 |
+
if link_prefix in item_link:
|
39 |
+
good_item = item
|
40 |
+
break
|
41 |
+
|
42 |
+
good_text = good_item.text
|
43 |
+
target_word = good_text.split(" - ")[-1].lower() # something like "topaz"
|
44 |
+
|
45 |
+
### Page header
|
46 |
+
st.title("Wordle Wizard Assistant π§π")
|
47 |
+
|
48 |
+
### Loading in official word list
|
49 |
+
official_words = []
|
50 |
+
with open("data/official_words_processed.txt", "r", encoding = "utf-8") as f:
|
51 |
+
for word in f.read().split("\n"):
|
52 |
+
if len(word) == 5:
|
53 |
+
official_words.append(word)
|
54 |
+
f.close() # closes connection to file
|
55 |
+
|
56 |
+
### Examples of words to use
|
57 |
+
sugg_words = []
|
58 |
+
for i in range(0, 20):
|
59 |
+
ran_int = random.randint(0, len(official_words) - 1)
|
60 |
+
word = official_words[ran_int]
|
61 |
+
sugg_words.append(word)
|
62 |
+
|
63 |
+
|
64 |
+
# ### for guess length validation of both guesses
|
65 |
+
# valid_guesses = True
|
66 |
+
|
67 |
+
# ### Generate Examples Button
|
68 |
+
# st.write('Please enter a starting word and a target word, and click the "Abracadabra" button to have the puzzle solved.\n')
|
69 |
+
# st.write('If you would like some examples of words you can use, click the button below.\n')
|
70 |
+
# # gen_egs = st.button('Show Me Words')
|
71 |
+
|
72 |
+
# if st.button('Show Me Words', key = "button1"):
|
73 |
+
# st.write(f"There are {len(official_words)} in the official Wordle word list. Here are {len(sugg_words)} of them.")
|
74 |
+
# st.write(f"{sugg_words}\n")
|
75 |
+
|
76 |
+
# # user starting word
|
77 |
+
# starting_word = st.text_input("Enter your first guess here here")
|
78 |
+
# starting_word = starting_word.strip().replace(" ", "").lower()
|
79 |
+
# if len(starting_word) != 5:
|
80 |
+
# valid_guesses = False
|
81 |
+
# st.write('Please double check and make sure there are exactly 5 letters in the starting word.\n')
|
82 |
+
|
83 |
+
# # user target word
|
84 |
+
# target_word = st.text_input("Enter target word here")
|
85 |
+
# target_word = target_word.strip().replace(" ", "").lower()
|
86 |
+
# if len(target_word) != 5:
|
87 |
+
# valid_guesses = False
|
88 |
+
# st.write('Please double check and make sure there are exactly 5 letters in the target word.\n')
|
89 |
+
|
90 |
+
|
91 |
+
#### USER PROVIDING GUESSES ####
|
92 |
+
num_guesses = st.sidebar.selectbox(
|
93 |
+
'How many guesses would you like to submit?',
|
94 |
+
(1, 2, 3, 4, 5))
|
95 |
+
|
96 |
+
guesses = []
|
97 |
+
for i in range(num_guesses):
|
98 |
+
input_guess = st.text_input(f"Guess #{i+1}", '')
|
99 |
+
guesses.append(input_guess)
|
100 |
+
st.write(f"Guess #1: {input_guess}")
|
101 |
+
|
102 |
+
#### CHECKING THAT ALL GUESSES ARE VALID
|
103 |
+
def is_alphanumeric_and_of_length_5(guess):
|
104 |
+
stripped_guess = guess.strip()
|
105 |
+
return stripped_guess.isalpha() and len(stripped_guess) == 5 # no punctuation, no numbers, 5 letters in length
|
106 |
+
|
107 |
+
# guesses = ["guess1", "guess 2", "guess3 ", " guess4", "guess5"]
|
108 |
+
valid_guesses = all(is_alphanumeric_and_of_length_5(guess) for guess in guesses)
|
109 |
+
|
110 |
+
### Solving
|
111 |
+
# solve_button = st.button('Abracadabra')
|
112 |
+
if button('Abracadabra', key = "button2"): # button to make everything run
|
113 |
+
|
114 |
+
#### CHECKING ALL GUESSES ARE LEGAL
|
115 |
+
if not valid_guesses:
|
116 |
+
st.write("Please check again that each guess only contains letters and is 5 letters in length. Once you have, click 'Abracadabra' to get feedback.")
|
117 |
+
|
118 |
+
else: # if everything is legal, proceed to solving
|
119 |
+
|
120 |
+
#### ADDING UNSEEN WORDS TO OFFICIAL LIST (THIS SHOULD MINIMIZE OVERALL ERRORS)
|
121 |
+
for word in guesses:
|
122 |
+
if word not in guesses:
|
123 |
+
official_words.append(word)
|
124 |
+
|
125 |
+
#### RUN ALGORITHM
|
126 |
+
wordle_wizard_cheat(guesses = guesses, word_list = official_words, max_guesses = 6,
|
127 |
+
target = target_word,
|
128 |
+
random_guess = False, random_target = False,
|
129 |
+
verbose = True, drama = 0, return_stats = False, record = False)
|
130 |
+
|
131 |
+
# post-solution prompt
|
132 |
+
st.write("Curious about what the number beside each word means? Click the button below to find out!")
|
133 |
+
|
134 |
+
# show plot and info
|
135 |
+
if button(label = "More info", key = "button3"):
|
136 |
+
|
137 |
+
# show plot of letters distribution
|
138 |
+
count_plot()
|
139 |
+
|
140 |
+
st.write("This is a distribution of the frequencies of all letters in the Wordle word list used in this app. The higher a given letter's count is, the more likely it is that that letter will be able to tell us something about the target word in a Wordle puzzle.\n")
|
141 |
+
st.write("The rating of each word corresponds to approximately the percentage of all words of the ~2300 words of the list used for this game in which the given word's letters appear. This means that, for a word with a rating of 30, its letters show up in 30\% of the words of the entire word list. Since we cannot possibly have all 26 letters of the English alphabet in one 5-letter word, this rating can only really be used to compare one word to another. Using more highly-rated words should generally result in getting to the target word in fewer guesses than using lower-rated words.\n")
|
142 |
+
|
143 |
+
# show plot of best and worst words
|
144 |
+
words_plot()
|
145 |
+
|
146 |
+
st.write("By this same rating system, here are the top 5 words, the middle 5 words, and the worst 5 words of the entire Wordle word list in terms of their respective ratings.\n\n")
|
147 |
+
st.write("If you're interested in learning more about the theory of how Wordle Wizard actually works, check out this blog post (https://medium.com/@kmaurinjones/how-i-beat-wordle-once-and-for-all-322c8641a70d), that describes everything mentioned here (and more!) in greater detail.\n")
|
148 |
+
|
149 |
+
st.write("-----------------------------\n")
|
150 |
+
|
151 |
+
st.write("\nThanks for checking out Wordle Wizard! If you have any feedback or requests for additions to this app, shoot me an email at kmaurinjones@gmail.com.")
|
data/official_words_processed.txt
ADDED
@@ -0,0 +1,2309 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
wince
|
2 |
+
thyme
|
3 |
+
mower
|
4 |
+
horde
|
5 |
+
heard
|
6 |
+
tenor
|
7 |
+
zonal
|
8 |
+
parry
|
9 |
+
shied
|
10 |
+
fizzy
|
11 |
+
flush
|
12 |
+
skunk
|
13 |
+
quilt
|
14 |
+
baton
|
15 |
+
cocoa
|
16 |
+
blend
|
17 |
+
haste
|
18 |
+
pansy
|
19 |
+
seize
|
20 |
+
thick
|
21 |
+
marsh
|
22 |
+
metal
|
23 |
+
eagle
|
24 |
+
droit
|
25 |
+
flock
|
26 |
+
legal
|
27 |
+
heart
|
28 |
+
posit
|
29 |
+
neigh
|
30 |
+
gamma
|
31 |
+
rabbi
|
32 |
+
wooer
|
33 |
+
ahead
|
34 |
+
horny
|
35 |
+
flaky
|
36 |
+
knave
|
37 |
+
probe
|
38 |
+
guilt
|
39 |
+
torus
|
40 |
+
berth
|
41 |
+
model
|
42 |
+
input
|
43 |
+
girly
|
44 |
+
tuber
|
45 |
+
booty
|
46 |
+
lurid
|
47 |
+
altar
|
48 |
+
scold
|
49 |
+
chock
|
50 |
+
crush
|
51 |
+
krill
|
52 |
+
toddy
|
53 |
+
rerun
|
54 |
+
poser
|
55 |
+
creme
|
56 |
+
bluer
|
57 |
+
slosh
|
58 |
+
sound
|
59 |
+
chess
|
60 |
+
lobby
|
61 |
+
viper
|
62 |
+
pesky
|
63 |
+
pleat
|
64 |
+
hunky
|
65 |
+
flick
|
66 |
+
lunge
|
67 |
+
cheek
|
68 |
+
forte
|
69 |
+
shady
|
70 |
+
purer
|
71 |
+
boozy
|
72 |
+
snack
|
73 |
+
debut
|
74 |
+
stump
|
75 |
+
enter
|
76 |
+
choir
|
77 |
+
broad
|
78 |
+
quash
|
79 |
+
exalt
|
80 |
+
smell
|
81 |
+
jewel
|
82 |
+
coupe
|
83 |
+
might
|
84 |
+
vinyl
|
85 |
+
sugar
|
86 |
+
gleam
|
87 |
+
deity
|
88 |
+
blame
|
89 |
+
finer
|
90 |
+
safer
|
91 |
+
cutie
|
92 |
+
thumb
|
93 |
+
tutor
|
94 |
+
grand
|
95 |
+
cache
|
96 |
+
camel
|
97 |
+
grace
|
98 |
+
pivot
|
99 |
+
apart
|
100 |
+
vegan
|
101 |
+
sense
|
102 |
+
queen
|
103 |
+
under
|
104 |
+
karma
|
105 |
+
duchy
|
106 |
+
smith
|
107 |
+
amend
|
108 |
+
drill
|
109 |
+
asset
|
110 |
+
awoke
|
111 |
+
aging
|
112 |
+
canon
|
113 |
+
wrong
|
114 |
+
glory
|
115 |
+
adorn
|
116 |
+
shelf
|
117 |
+
repay
|
118 |
+
rayon
|
119 |
+
briny
|
120 |
+
eying
|
121 |
+
unify
|
122 |
+
yield
|
123 |
+
leant
|
124 |
+
cabin
|
125 |
+
merry
|
126 |
+
strap
|
127 |
+
bunny
|
128 |
+
snare
|
129 |
+
jumpy
|
130 |
+
fable
|
131 |
+
beget
|
132 |
+
moody
|
133 |
+
craze
|
134 |
+
unset
|
135 |
+
furor
|
136 |
+
slant
|
137 |
+
sloth
|
138 |
+
brain
|
139 |
+
poppy
|
140 |
+
crump
|
141 |
+
decal
|
142 |
+
drool
|
143 |
+
thank
|
144 |
+
tiger
|
145 |
+
lover
|
146 |
+
crank
|
147 |
+
voter
|
148 |
+
mound
|
149 |
+
slick
|
150 |
+
birch
|
151 |
+
aping
|
152 |
+
impel
|
153 |
+
demur
|
154 |
+
close
|
155 |
+
fudge
|
156 |
+
sonar
|
157 |
+
aloof
|
158 |
+
baron
|
159 |
+
bevel
|
160 |
+
tread
|
161 |
+
iliac
|
162 |
+
scaly
|
163 |
+
rusty
|
164 |
+
frond
|
165 |
+
sedan
|
166 |
+
skulk
|
167 |
+
flail
|
168 |
+
tunic
|
169 |
+
snuff
|
170 |
+
dirty
|
171 |
+
hello
|
172 |
+
stare
|
173 |
+
sloop
|
174 |
+
madly
|
175 |
+
proxy
|
176 |
+
ebony
|
177 |
+
fully
|
178 |
+
omega
|
179 |
+
ghoul
|
180 |
+
woven
|
181 |
+
quite
|
182 |
+
stamp
|
183 |
+
brake
|
184 |
+
natal
|
185 |
+
valet
|
186 |
+
augur
|
187 |
+
shall
|
188 |
+
wrist
|
189 |
+
harpy
|
190 |
+
swoop
|
191 |
+
motel
|
192 |
+
foamy
|
193 |
+
coast
|
194 |
+
yacht
|
195 |
+
lusty
|
196 |
+
there
|
197 |
+
nasal
|
198 |
+
bleep
|
199 |
+
rearm
|
200 |
+
staid
|
201 |
+
tonic
|
202 |
+
cheap
|
203 |
+
attic
|
204 |
+
enemy
|
205 |
+
abode
|
206 |
+
being
|
207 |
+
sheep
|
208 |
+
canoe
|
209 |
+
derby
|
210 |
+
recap
|
211 |
+
merge
|
212 |
+
crypt
|
213 |
+
fatal
|
214 |
+
block
|
215 |
+
pubic
|
216 |
+
disco
|
217 |
+
kitty
|
218 |
+
heavy
|
219 |
+
naive
|
220 |
+
sight
|
221 |
+
threw
|
222 |
+
crawl
|
223 |
+
known
|
224 |
+
torso
|
225 |
+
thief
|
226 |
+
navel
|
227 |
+
slack
|
228 |
+
crowd
|
229 |
+
paint
|
230 |
+
champ
|
231 |
+
butch
|
232 |
+
nutty
|
233 |
+
thigh
|
234 |
+
intro
|
235 |
+
order
|
236 |
+
turbo
|
237 |
+
tacky
|
238 |
+
enema
|
239 |
+
frown
|
240 |
+
slime
|
241 |
+
shear
|
242 |
+
ester
|
243 |
+
organ
|
244 |
+
revel
|
245 |
+
bunch
|
246 |
+
spoof
|
247 |
+
color
|
248 |
+
mango
|
249 |
+
vouch
|
250 |
+
crate
|
251 |
+
scope
|
252 |
+
hinge
|
253 |
+
north
|
254 |
+
tapir
|
255 |
+
plumb
|
256 |
+
folly
|
257 |
+
ionic
|
258 |
+
spawn
|
259 |
+
affix
|
260 |
+
total
|
261 |
+
wedge
|
262 |
+
broom
|
263 |
+
satin
|
264 |
+
ozone
|
265 |
+
polyp
|
266 |
+
tilde
|
267 |
+
faith
|
268 |
+
blitz
|
269 |
+
whole
|
270 |
+
roast
|
271 |
+
anime
|
272 |
+
gusto
|
273 |
+
pluck
|
274 |
+
cavil
|
275 |
+
tipsy
|
276 |
+
newer
|
277 |
+
board
|
278 |
+
nudge
|
279 |
+
twirl
|
280 |
+
lever
|
281 |
+
honey
|
282 |
+
tempo
|
283 |
+
scare
|
284 |
+
semen
|
285 |
+
built
|
286 |
+
chime
|
287 |
+
crust
|
288 |
+
admin
|
289 |
+
diary
|
290 |
+
clear
|
291 |
+
guise
|
292 |
+
cough
|
293 |
+
baker
|
294 |
+
evict
|
295 |
+
moral
|
296 |
+
piper
|
297 |
+
libel
|
298 |
+
sweet
|
299 |
+
wrung
|
300 |
+
after
|
301 |
+
gazer
|
302 |
+
drawn
|
303 |
+
torch
|
304 |
+
lying
|
305 |
+
ethos
|
306 |
+
filth
|
307 |
+
siren
|
308 |
+
flake
|
309 |
+
still
|
310 |
+
knoll
|
311 |
+
udder
|
312 |
+
shard
|
313 |
+
drone
|
314 |
+
sally
|
315 |
+
style
|
316 |
+
hutch
|
317 |
+
stalk
|
318 |
+
loamy
|
319 |
+
relay
|
320 |
+
crude
|
321 |
+
atone
|
322 |
+
relax
|
323 |
+
cover
|
324 |
+
comic
|
325 |
+
blurb
|
326 |
+
point
|
327 |
+
brass
|
328 |
+
jerky
|
329 |
+
equip
|
330 |
+
rinse
|
331 |
+
trait
|
332 |
+
knife
|
333 |
+
since
|
334 |
+
swear
|
335 |
+
sword
|
336 |
+
trope
|
337 |
+
aider
|
338 |
+
vigor
|
339 |
+
youth
|
340 |
+
rebel
|
341 |
+
chart
|
342 |
+
rivet
|
343 |
+
islet
|
344 |
+
rival
|
345 |
+
dodge
|
346 |
+
human
|
347 |
+
gummy
|
348 |
+
preen
|
349 |
+
baste
|
350 |
+
awash
|
351 |
+
inert
|
352 |
+
reach
|
353 |
+
chili
|
354 |
+
carve
|
355 |
+
chant
|
356 |
+
wield
|
357 |
+
floor
|
358 |
+
agree
|
359 |
+
stomp
|
360 |
+
clump
|
361 |
+
suing
|
362 |
+
aptly
|
363 |
+
spare
|
364 |
+
conic
|
365 |
+
shape
|
366 |
+
gouge
|
367 |
+
adopt
|
368 |
+
flash
|
369 |
+
blood
|
370 |
+
grape
|
371 |
+
gross
|
372 |
+
peril
|
373 |
+
chase
|
374 |
+
snarl
|
375 |
+
burst
|
376 |
+
fetch
|
377 |
+
mucky
|
378 |
+
retry
|
379 |
+
shrub
|
380 |
+
shake
|
381 |
+
pesto
|
382 |
+
begun
|
383 |
+
toxic
|
384 |
+
sassy
|
385 |
+
sorry
|
386 |
+
quasi
|
387 |
+
cadet
|
388 |
+
mucus
|
389 |
+
scant
|
390 |
+
moose
|
391 |
+
admit
|
392 |
+
trout
|
393 |
+
upset
|
394 |
+
album
|
395 |
+
burnt
|
396 |
+
dwell
|
397 |
+
lumpy
|
398 |
+
tight
|
399 |
+
civic
|
400 |
+
sewer
|
401 |
+
flier
|
402 |
+
recut
|
403 |
+
inlet
|
404 |
+
syrup
|
405 |
+
scuba
|
406 |
+
abbot
|
407 |
+
brook
|
408 |
+
feign
|
409 |
+
cheat
|
410 |
+
embed
|
411 |
+
furry
|
412 |
+
scent
|
413 |
+
mirth
|
414 |
+
surly
|
415 |
+
bison
|
416 |
+
mambo
|
417 |
+
quell
|
418 |
+
tubal
|
419 |
+
husky
|
420 |
+
plane
|
421 |
+
prowl
|
422 |
+
abled
|
423 |
+
angle
|
424 |
+
later
|
425 |
+
baggy
|
426 |
+
tabby
|
427 |
+
amble
|
428 |
+
cloth
|
429 |
+
wispy
|
430 |
+
rarer
|
431 |
+
blink
|
432 |
+
amuse
|
433 |
+
flute
|
434 |
+
stony
|
435 |
+
plaid
|
436 |
+
epoch
|
437 |
+
truck
|
438 |
+
saute
|
439 |
+
groom
|
440 |
+
sixth
|
441 |
+
chair
|
442 |
+
fruit
|
443 |
+
tying
|
444 |
+
easel
|
445 |
+
humus
|
446 |
+
entry
|
447 |
+
butte
|
448 |
+
lapel
|
449 |
+
eject
|
450 |
+
cramp
|
451 |
+
cumin
|
452 |
+
eking
|
453 |
+
tooth
|
454 |
+
rebus
|
455 |
+
great
|
456 |
+
blind
|
457 |
+
broth
|
458 |
+
ocean
|
459 |
+
stern
|
460 |
+
inner
|
461 |
+
shell
|
462 |
+
timid
|
463 |
+
tonal
|
464 |
+
south
|
465 |
+
hymen
|
466 |
+
drake
|
467 |
+
smite
|
468 |
+
scrap
|
469 |
+
wheat
|
470 |
+
dense
|
471 |
+
labor
|
472 |
+
gipsy
|
473 |
+
sumac
|
474 |
+
issue
|
475 |
+
leggy
|
476 |
+
skiff
|
477 |
+
earth
|
478 |
+
spelt
|
479 |
+
miser
|
480 |
+
smile
|
481 |
+
sushi
|
482 |
+
minty
|
483 |
+
smack
|
484 |
+
maxim
|
485 |
+
taste
|
486 |
+
taffy
|
487 |
+
patio
|
488 |
+
steer
|
489 |
+
party
|
490 |
+
clued
|
491 |
+
genre
|
492 |
+
druid
|
493 |
+
widen
|
494 |
+
remit
|
495 |
+
agile
|
496 |
+
parka
|
497 |
+
dusky
|
498 |
+
savor
|
499 |
+
caper
|
500 |
+
ninny
|
501 |
+
motto
|
502 |
+
foist
|
503 |
+
prune
|
504 |
+
blimp
|
505 |
+
revue
|
506 |
+
ounce
|
507 |
+
grill
|
508 |
+
cruel
|
509 |
+
melee
|
510 |
+
ovoid
|
511 |
+
rifle
|
512 |
+
agent
|
513 |
+
mania
|
514 |
+
blond
|
515 |
+
fixer
|
516 |
+
lemon
|
517 |
+
cater
|
518 |
+
sprig
|
519 |
+
rupee
|
520 |
+
roger
|
521 |
+
weave
|
522 |
+
begin
|
523 |
+
steel
|
524 |
+
pudgy
|
525 |
+
lager
|
526 |
+
chick
|
527 |
+
paste
|
528 |
+
booby
|
529 |
+
ridge
|
530 |
+
prize
|
531 |
+
audit
|
532 |
+
swept
|
533 |
+
meaty
|
534 |
+
siege
|
535 |
+
humor
|
536 |
+
kneel
|
537 |
+
grave
|
538 |
+
lasso
|
539 |
+
comfy
|
540 |
+
below
|
541 |
+
chief
|
542 |
+
tough
|
543 |
+
mercy
|
544 |
+
found
|
545 |
+
dryer
|
546 |
+
valid
|
547 |
+
grail
|
548 |
+
wrest
|
549 |
+
humph
|
550 |
+
nadir
|
551 |
+
awful
|
552 |
+
fiber
|
553 |
+
short
|
554 |
+
exact
|
555 |
+
adage
|
556 |
+
swath
|
557 |
+
gorge
|
558 |
+
slang
|
559 |
+
notch
|
560 |
+
carol
|
561 |
+
alarm
|
562 |
+
marry
|
563 |
+
assay
|
564 |
+
jetty
|
565 |
+
roach
|
566 |
+
edify
|
567 |
+
juice
|
568 |
+
chain
|
569 |
+
gravy
|
570 |
+
shout
|
571 |
+
abate
|
572 |
+
scour
|
573 |
+
elfin
|
574 |
+
rhino
|
575 |
+
defer
|
576 |
+
pried
|
577 |
+
amaze
|
578 |
+
dingy
|
579 |
+
drown
|
580 |
+
arose
|
581 |
+
wager
|
582 |
+
snail
|
583 |
+
antic
|
584 |
+
hurry
|
585 |
+
guppy
|
586 |
+
dozen
|
587 |
+
louse
|
588 |
+
filer
|
589 |
+
swung
|
590 |
+
moldy
|
591 |
+
hyena
|
592 |
+
milky
|
593 |
+
sepia
|
594 |
+
forum
|
595 |
+
fiend
|
596 |
+
outer
|
597 |
+
anode
|
598 |
+
flown
|
599 |
+
stoop
|
600 |
+
cabal
|
601 |
+
cobra
|
602 |
+
dully
|
603 |
+
hardy
|
604 |
+
ensue
|
605 |
+
knelt
|
606 |
+
stool
|
607 |
+
worry
|
608 |
+
slash
|
609 |
+
digit
|
610 |
+
drawl
|
611 |
+
perky
|
612 |
+
glint
|
613 |
+
first
|
614 |
+
dandy
|
615 |
+
coyly
|
616 |
+
scowl
|
617 |
+
tidal
|
618 |
+
raspy
|
619 |
+
stilt
|
620 |
+
dwarf
|
621 |
+
whirl
|
622 |
+
stunt
|
623 |
+
diode
|
624 |
+
hasty
|
625 |
+
glaze
|
626 |
+
storm
|
627 |
+
scary
|
628 |
+
salve
|
629 |
+
banal
|
630 |
+
bloke
|
631 |
+
right
|
632 |
+
brine
|
633 |
+
biddy
|
634 |
+
comet
|
635 |
+
erode
|
636 |
+
quirk
|
637 |
+
shone
|
638 |
+
venue
|
639 |
+
fever
|
640 |
+
start
|
641 |
+
suave
|
642 |
+
stave
|
643 |
+
rodeo
|
644 |
+
solve
|
645 |
+
parer
|
646 |
+
juicy
|
647 |
+
occur
|
648 |
+
scene
|
649 |
+
above
|
650 |
+
puppy
|
651 |
+
surer
|
652 |
+
dimly
|
653 |
+
imply
|
654 |
+
elite
|
655 |
+
arise
|
656 |
+
drunk
|
657 |
+
react
|
658 |
+
hatch
|
659 |
+
leach
|
660 |
+
smelt
|
661 |
+
focal
|
662 |
+
dough
|
663 |
+
pinto
|
664 |
+
jazzy
|
665 |
+
otter
|
666 |
+
plait
|
667 |
+
reply
|
668 |
+
flunk
|
669 |
+
irony
|
670 |
+
manga
|
671 |
+
freer
|
672 |
+
spiel
|
673 |
+
wharf
|
674 |
+
these
|
675 |
+
retch
|
676 |
+
third
|
677 |
+
whine
|
678 |
+
eight
|
679 |
+
nerdy
|
680 |
+
hence
|
681 |
+
worst
|
682 |
+
uncle
|
683 |
+
motor
|
684 |
+
flyer
|
685 |
+
curse
|
686 |
+
askew
|
687 |
+
strut
|
688 |
+
wheel
|
689 |
+
lingo
|
690 |
+
sadly
|
691 |
+
goner
|
692 |
+
poise
|
693 |
+
shove
|
694 |
+
buddy
|
695 |
+
spell
|
696 |
+
awake
|
697 |
+
midge
|
698 |
+
crier
|
699 |
+
drier
|
700 |
+
tango
|
701 |
+
grout
|
702 |
+
stunk
|
703 |
+
purge
|
704 |
+
lymph
|
705 |
+
lucid
|
706 |
+
sheer
|
707 |
+
fjord
|
708 |
+
dowry
|
709 |
+
brink
|
710 |
+
liege
|
711 |
+
loose
|
712 |
+
posse
|
713 |
+
prank
|
714 |
+
movie
|
715 |
+
shorn
|
716 |
+
place
|
717 |
+
bless
|
718 |
+
unmet
|
719 |
+
elope
|
720 |
+
shade
|
721 |
+
bylaw
|
722 |
+
hedge
|
723 |
+
bicep
|
724 |
+
uncut
|
725 |
+
fluke
|
726 |
+
month
|
727 |
+
space
|
728 |
+
speck
|
729 |
+
denim
|
730 |
+
chaos
|
731 |
+
civil
|
732 |
+
avoid
|
733 |
+
dryly
|
734 |
+
brand
|
735 |
+
fence
|
736 |
+
chirp
|
737 |
+
daddy
|
738 |
+
exist
|
739 |
+
cluck
|
740 |
+
amass
|
741 |
+
perch
|
742 |
+
spiny
|
743 |
+
curvy
|
744 |
+
front
|
745 |
+
thong
|
746 |
+
fifty
|
747 |
+
borax
|
748 |
+
noble
|
749 |
+
curio
|
750 |
+
death
|
751 |
+
bough
|
752 |
+
limbo
|
753 |
+
bathe
|
754 |
+
timer
|
755 |
+
rugby
|
756 |
+
vault
|
757 |
+
panel
|
758 |
+
giddy
|
759 |
+
learn
|
760 |
+
spend
|
761 |
+
retro
|
762 |
+
leery
|
763 |
+
nasty
|
764 |
+
bitty
|
765 |
+
women
|
766 |
+
ranch
|
767 |
+
tamer
|
768 |
+
gauge
|
769 |
+
thump
|
770 |
+
booth
|
771 |
+
elder
|
772 |
+
devil
|
773 |
+
myrrh
|
774 |
+
geese
|
775 |
+
grass
|
776 |
+
trove
|
777 |
+
pause
|
778 |
+
molar
|
779 |
+
octal
|
780 |
+
brawl
|
781 |
+
email
|
782 |
+
build
|
783 |
+
early
|
784 |
+
guess
|
785 |
+
scone
|
786 |
+
weird
|
787 |
+
cress
|
788 |
+
chafe
|
789 |
+
noisy
|
790 |
+
guard
|
791 |
+
cleft
|
792 |
+
paler
|
793 |
+
brawn
|
794 |
+
inlay
|
795 |
+
robot
|
796 |
+
price
|
797 |
+
vying
|
798 |
+
climb
|
799 |
+
brute
|
800 |
+
liver
|
801 |
+
serve
|
802 |
+
emcee
|
803 |
+
topaz
|
804 |
+
cheer
|
805 |
+
alibi
|
806 |
+
quake
|
807 |
+
chard
|
808 |
+
motif
|
809 |
+
flour
|
810 |
+
joint
|
811 |
+
punch
|
812 |
+
facet
|
813 |
+
manor
|
814 |
+
pence
|
815 |
+
bleat
|
816 |
+
vowel
|
817 |
+
froze
|
818 |
+
cigar
|
819 |
+
trump
|
820 |
+
overt
|
821 |
+
least
|
822 |
+
final
|
823 |
+
sever
|
824 |
+
aloft
|
825 |
+
idler
|
826 |
+
roomy
|
827 |
+
rebar
|
828 |
+
inane
|
829 |
+
joist
|
830 |
+
elect
|
831 |
+
stung
|
832 |
+
surge
|
833 |
+
primo
|
834 |
+
nicer
|
835 |
+
river
|
836 |
+
feral
|
837 |
+
scrum
|
838 |
+
dizzy
|
839 |
+
crave
|
840 |
+
dross
|
841 |
+
video
|
842 |
+
belie
|
843 |
+
spoon
|
844 |
+
mayor
|
845 |
+
ramen
|
846 |
+
vocal
|
847 |
+
spool
|
848 |
+
curly
|
849 |
+
glove
|
850 |
+
equal
|
851 |
+
terra
|
852 |
+
murky
|
853 |
+
check
|
854 |
+
ripen
|
855 |
+
white
|
856 |
+
proof
|
857 |
+
belch
|
858 |
+
began
|
859 |
+
rural
|
860 |
+
query
|
861 |
+
vague
|
862 |
+
dance
|
863 |
+
chose
|
864 |
+
windy
|
865 |
+
gland
|
866 |
+
maize
|
867 |
+
viral
|
868 |
+
burly
|
869 |
+
crisp
|
870 |
+
pinky
|
871 |
+
slunk
|
872 |
+
wring
|
873 |
+
verge
|
874 |
+
pitch
|
875 |
+
graph
|
876 |
+
trace
|
877 |
+
pixel
|
878 |
+
cried
|
879 |
+
stove
|
880 |
+
slush
|
881 |
+
crimp
|
882 |
+
lumen
|
883 |
+
brisk
|
884 |
+
heave
|
885 |
+
tepid
|
886 |
+
reset
|
887 |
+
flank
|
888 |
+
moist
|
889 |
+
totem
|
890 |
+
fussy
|
891 |
+
blade
|
892 |
+
circa
|
893 |
+
apple
|
894 |
+
night
|
895 |
+
spoke
|
896 |
+
rover
|
897 |
+
oxide
|
898 |
+
scalp
|
899 |
+
rumba
|
900 |
+
whiff
|
901 |
+
aunty
|
902 |
+
sauce
|
903 |
+
often
|
904 |
+
rowdy
|
905 |
+
prime
|
906 |
+
union
|
907 |
+
downy
|
908 |
+
apron
|
909 |
+
wrath
|
910 |
+
ladle
|
911 |
+
beard
|
912 |
+
stead
|
913 |
+
tardy
|
914 |
+
sixty
|
915 |
+
slain
|
916 |
+
glare
|
917 |
+
yeast
|
918 |
+
terse
|
919 |
+
vicar
|
920 |
+
hoard
|
921 |
+
bride
|
922 |
+
vigil
|
923 |
+
tasty
|
924 |
+
utter
|
925 |
+
mossy
|
926 |
+
spiky
|
927 |
+
boast
|
928 |
+
swami
|
929 |
+
brace
|
930 |
+
until
|
931 |
+
woozy
|
932 |
+
shirk
|
933 |
+
pithy
|
934 |
+
depth
|
935 |
+
giver
|
936 |
+
sonic
|
937 |
+
snake
|
938 |
+
field
|
939 |
+
sheik
|
940 |
+
leafy
|
941 |
+
offer
|
942 |
+
slimy
|
943 |
+
stint
|
944 |
+
ruddy
|
945 |
+
taper
|
946 |
+
kinky
|
947 |
+
stoke
|
948 |
+
sappy
|
949 |
+
sweep
|
950 |
+
smock
|
951 |
+
humid
|
952 |
+
steak
|
953 |
+
leech
|
954 |
+
gonad
|
955 |
+
leaky
|
956 |
+
shuck
|
957 |
+
route
|
958 |
+
piano
|
959 |
+
umbra
|
960 |
+
penny
|
961 |
+
kebab
|
962 |
+
money
|
963 |
+
betel
|
964 |
+
unwed
|
965 |
+
beast
|
966 |
+
cinch
|
967 |
+
snuck
|
968 |
+
maker
|
969 |
+
sheet
|
970 |
+
fried
|
971 |
+
fiery
|
972 |
+
latte
|
973 |
+
bliss
|
974 |
+
debit
|
975 |
+
grasp
|
976 |
+
mulch
|
977 |
+
spied
|
978 |
+
tease
|
979 |
+
judge
|
980 |
+
corny
|
981 |
+
write
|
982 |
+
sooth
|
983 |
+
trick
|
984 |
+
clean
|
985 |
+
cubic
|
986 |
+
fetid
|
987 |
+
skimp
|
988 |
+
eerie
|
989 |
+
prick
|
990 |
+
piety
|
991 |
+
sport
|
992 |
+
vital
|
993 |
+
train
|
994 |
+
lefty
|
995 |
+
hotel
|
996 |
+
micro
|
997 |
+
graft
|
998 |
+
spear
|
999 |
+
opium
|
1000 |
+
tenth
|
1001 |
+
catch
|
1002 |
+
waver
|
1003 |
+
outdo
|
1004 |
+
bravo
|
1005 |
+
delve
|
1006 |
+
craft
|
1007 |
+
tumor
|
1008 |
+
carat
|
1009 |
+
slate
|
1010 |
+
again
|
1011 |
+
liner
|
1012 |
+
crime
|
1013 |
+
baler
|
1014 |
+
hound
|
1015 |
+
macaw
|
1016 |
+
drain
|
1017 |
+
rainy
|
1018 |
+
store
|
1019 |
+
could
|
1020 |
+
hotly
|
1021 |
+
morph
|
1022 |
+
snide
|
1023 |
+
meant
|
1024 |
+
gamut
|
1025 |
+
daisy
|
1026 |
+
teach
|
1027 |
+
boxer
|
1028 |
+
shoal
|
1029 |
+
crony
|
1030 |
+
ulcer
|
1031 |
+
sandy
|
1032 |
+
squat
|
1033 |
+
payee
|
1034 |
+
skull
|
1035 |
+
villa
|
1036 |
+
class
|
1037 |
+
nerve
|
1038 |
+
porch
|
1039 |
+
scram
|
1040 |
+
wacky
|
1041 |
+
undue
|
1042 |
+
jumbo
|
1043 |
+
satyr
|
1044 |
+
aloud
|
1045 |
+
raven
|
1046 |
+
detox
|
1047 |
+
mason
|
1048 |
+
taker
|
1049 |
+
boule
|
1050 |
+
croup
|
1051 |
+
junto
|
1052 |
+
pupil
|
1053 |
+
slurp
|
1054 |
+
belle
|
1055 |
+
scoff
|
1056 |
+
track
|
1057 |
+
trail
|
1058 |
+
polar
|
1059 |
+
lucky
|
1060 |
+
robin
|
1061 |
+
major
|
1062 |
+
snaky
|
1063 |
+
roost
|
1064 |
+
stoic
|
1065 |
+
corer
|
1066 |
+
blunt
|
1067 |
+
group
|
1068 |
+
gayly
|
1069 |
+
blaze
|
1070 |
+
truer
|
1071 |
+
winch
|
1072 |
+
wreck
|
1073 |
+
going
|
1074 |
+
pinch
|
1075 |
+
covey
|
1076 |
+
plier
|
1077 |
+
spicy
|
1078 |
+
bench
|
1079 |
+
caste
|
1080 |
+
cello
|
1081 |
+
spike
|
1082 |
+
polka
|
1083 |
+
ratty
|
1084 |
+
lunch
|
1085 |
+
serum
|
1086 |
+
piece
|
1087 |
+
royal
|
1088 |
+
world
|
1089 |
+
evoke
|
1090 |
+
algae
|
1091 |
+
adapt
|
1092 |
+
shine
|
1093 |
+
mural
|
1094 |
+
gooey
|
1095 |
+
unfit
|
1096 |
+
kiosk
|
1097 |
+
icing
|
1098 |
+
spill
|
1099 |
+
shore
|
1100 |
+
helix
|
1101 |
+
cross
|
1102 |
+
gaffe
|
1103 |
+
waltz
|
1104 |
+
throb
|
1105 |
+
would
|
1106 |
+
piney
|
1107 |
+
quote
|
1108 |
+
snipe
|
1109 |
+
annoy
|
1110 |
+
wiser
|
1111 |
+
whisk
|
1112 |
+
slept
|
1113 |
+
twist
|
1114 |
+
epoxy
|
1115 |
+
covet
|
1116 |
+
fuzzy
|
1117 |
+
frame
|
1118 |
+
harry
|
1119 |
+
pique
|
1120 |
+
skier
|
1121 |
+
ovine
|
1122 |
+
vomit
|
1123 |
+
octet
|
1124 |
+
rocky
|
1125 |
+
drank
|
1126 |
+
handy
|
1127 |
+
ideal
|
1128 |
+
bushy
|
1129 |
+
shame
|
1130 |
+
sling
|
1131 |
+
sneak
|
1132 |
+
peach
|
1133 |
+
flare
|
1134 |
+
swash
|
1135 |
+
outgo
|
1136 |
+
trunk
|
1137 |
+
stiff
|
1138 |
+
basic
|
1139 |
+
dumpy
|
1140 |
+
rebut
|
1141 |
+
shale
|
1142 |
+
joust
|
1143 |
+
binge
|
1144 |
+
plain
|
1145 |
+
shire
|
1146 |
+
sieve
|
1147 |
+
titan
|
1148 |
+
gavel
|
1149 |
+
wafer
|
1150 |
+
freed
|
1151 |
+
refit
|
1152 |
+
putty
|
1153 |
+
munch
|
1154 |
+
madam
|
1155 |
+
poker
|
1156 |
+
gloss
|
1157 |
+
blurt
|
1158 |
+
stack
|
1159 |
+
shank
|
1160 |
+
layer
|
1161 |
+
testy
|
1162 |
+
oaken
|
1163 |
+
verso
|
1164 |
+
fault
|
1165 |
+
holly
|
1166 |
+
risen
|
1167 |
+
juror
|
1168 |
+
dopey
|
1169 |
+
width
|
1170 |
+
wreak
|
1171 |
+
theme
|
1172 |
+
agony
|
1173 |
+
snout
|
1174 |
+
lathe
|
1175 |
+
trust
|
1176 |
+
pizza
|
1177 |
+
voila
|
1178 |
+
decay
|
1179 |
+
relic
|
1180 |
+
gourd
|
1181 |
+
ficus
|
1182 |
+
alone
|
1183 |
+
viola
|
1184 |
+
swill
|
1185 |
+
creek
|
1186 |
+
stood
|
1187 |
+
guest
|
1188 |
+
moron
|
1189 |
+
apply
|
1190 |
+
mushy
|
1191 |
+
ratio
|
1192 |
+
chuck
|
1193 |
+
fetus
|
1194 |
+
proud
|
1195 |
+
droop
|
1196 |
+
koala
|
1197 |
+
whose
|
1198 |
+
pulse
|
1199 |
+
teddy
|
1200 |
+
slice
|
1201 |
+
beach
|
1202 |
+
fluff
|
1203 |
+
brash
|
1204 |
+
serif
|
1205 |
+
batch
|
1206 |
+
canal
|
1207 |
+
plume
|
1208 |
+
dilly
|
1209 |
+
trial
|
1210 |
+
braid
|
1211 |
+
naval
|
1212 |
+
treat
|
1213 |
+
shawl
|
1214 |
+
claim
|
1215 |
+
finch
|
1216 |
+
imbue
|
1217 |
+
arbor
|
1218 |
+
randy
|
1219 |
+
taken
|
1220 |
+
clasp
|
1221 |
+
macho
|
1222 |
+
sniff
|
1223 |
+
brown
|
1224 |
+
ascot
|
1225 |
+
clout
|
1226 |
+
lupus
|
1227 |
+
bossy
|
1228 |
+
pulpy
|
1229 |
+
biome
|
1230 |
+
tripe
|
1231 |
+
chump
|
1232 |
+
verse
|
1233 |
+
swift
|
1234 |
+
pedal
|
1235 |
+
smear
|
1236 |
+
abbey
|
1237 |
+
azure
|
1238 |
+
dally
|
1239 |
+
ditto
|
1240 |
+
haute
|
1241 |
+
hefty
|
1242 |
+
alert
|
1243 |
+
fugue
|
1244 |
+
curve
|
1245 |
+
droll
|
1246 |
+
while
|
1247 |
+
spout
|
1248 |
+
recur
|
1249 |
+
basil
|
1250 |
+
drive
|
1251 |
+
flora
|
1252 |
+
fungi
|
1253 |
+
payer
|
1254 |
+
bully
|
1255 |
+
vogue
|
1256 |
+
amber
|
1257 |
+
drove
|
1258 |
+
edict
|
1259 |
+
zesty
|
1260 |
+
dunce
|
1261 |
+
stork
|
1262 |
+
valor
|
1263 |
+
filet
|
1264 |
+
heron
|
1265 |
+
grade
|
1266 |
+
mimic
|
1267 |
+
actor
|
1268 |
+
cacao
|
1269 |
+
fanny
|
1270 |
+
hilly
|
1271 |
+
quill
|
1272 |
+
watch
|
1273 |
+
plead
|
1274 |
+
maybe
|
1275 |
+
fauna
|
1276 |
+
magic
|
1277 |
+
couch
|
1278 |
+
loyal
|
1279 |
+
mecca
|
1280 |
+
bawdy
|
1281 |
+
swine
|
1282 |
+
enjoy
|
1283 |
+
hairy
|
1284 |
+
grunt
|
1285 |
+
brave
|
1286 |
+
idyll
|
1287 |
+
alpha
|
1288 |
+
dowel
|
1289 |
+
evade
|
1290 |
+
rajah
|
1291 |
+
exile
|
1292 |
+
rogue
|
1293 |
+
adobe
|
1294 |
+
eater
|
1295 |
+
wrote
|
1296 |
+
other
|
1297 |
+
foyer
|
1298 |
+
amply
|
1299 |
+
havoc
|
1300 |
+
elate
|
1301 |
+
artsy
|
1302 |
+
study
|
1303 |
+
privy
|
1304 |
+
beret
|
1305 |
+
throw
|
1306 |
+
deter
|
1307 |
+
allot
|
1308 |
+
tract
|
1309 |
+
which
|
1310 |
+
bloat
|
1311 |
+
plate
|
1312 |
+
fleck
|
1313 |
+
solid
|
1314 |
+
frank
|
1315 |
+
diver
|
1316 |
+
fraud
|
1317 |
+
dolly
|
1318 |
+
truce
|
1319 |
+
harsh
|
1320 |
+
squib
|
1321 |
+
tried
|
1322 |
+
delay
|
1323 |
+
bowel
|
1324 |
+
freak
|
1325 |
+
quoth
|
1326 |
+
balmy
|
1327 |
+
gumbo
|
1328 |
+
waive
|
1329 |
+
liken
|
1330 |
+
frill
|
1331 |
+
incur
|
1332 |
+
jiffy
|
1333 |
+
weigh
|
1334 |
+
batty
|
1335 |
+
bleak
|
1336 |
+
welsh
|
1337 |
+
their
|
1338 |
+
merit
|
1339 |
+
leper
|
1340 |
+
rally
|
1341 |
+
sworn
|
1342 |
+
virus
|
1343 |
+
opine
|
1344 |
+
thorn
|
1345 |
+
jelly
|
1346 |
+
funny
|
1347 |
+
happy
|
1348 |
+
flint
|
1349 |
+
vapor
|
1350 |
+
array
|
1351 |
+
lodge
|
1352 |
+
brood
|
1353 |
+
gulch
|
1354 |
+
feast
|
1355 |
+
talon
|
1356 |
+
renal
|
1357 |
+
frock
|
1358 |
+
sulky
|
1359 |
+
green
|
1360 |
+
spice
|
1361 |
+
tibia
|
1362 |
+
idiom
|
1363 |
+
flame
|
1364 |
+
shock
|
1365 |
+
crepe
|
1366 |
+
phase
|
1367 |
+
spark
|
1368 |
+
grind
|
1369 |
+
haven
|
1370 |
+
forge
|
1371 |
+
media
|
1372 |
+
salad
|
1373 |
+
bused
|
1374 |
+
abuse
|
1375 |
+
seven
|
1376 |
+
large
|
1377 |
+
mangy
|
1378 |
+
round
|
1379 |
+
swish
|
1380 |
+
stole
|
1381 |
+
gaudy
|
1382 |
+
fleet
|
1383 |
+
tryst
|
1384 |
+
elegy
|
1385 |
+
score
|
1386 |
+
petal
|
1387 |
+
tulip
|
1388 |
+
sigma
|
1389 |
+
armor
|
1390 |
+
segue
|
1391 |
+
fancy
|
1392 |
+
dogma
|
1393 |
+
mince
|
1394 |
+
pagan
|
1395 |
+
angst
|
1396 |
+
slump
|
1397 |
+
quest
|
1398 |
+
state
|
1399 |
+
hater
|
1400 |
+
fight
|
1401 |
+
china
|
1402 |
+
olden
|
1403 |
+
unite
|
1404 |
+
saner
|
1405 |
+
paper
|
1406 |
+
heist
|
1407 |
+
stark
|
1408 |
+
clang
|
1409 |
+
gamer
|
1410 |
+
qualm
|
1411 |
+
dying
|
1412 |
+
eaten
|
1413 |
+
gloom
|
1414 |
+
smoke
|
1415 |
+
hitch
|
1416 |
+
quark
|
1417 |
+
aphid
|
1418 |
+
fairy
|
1419 |
+
anvil
|
1420 |
+
curry
|
1421 |
+
decor
|
1422 |
+
minus
|
1423 |
+
quota
|
1424 |
+
goose
|
1425 |
+
radar
|
1426 |
+
lance
|
1427 |
+
reign
|
1428 |
+
sleet
|
1429 |
+
suite
|
1430 |
+
value
|
1431 |
+
click
|
1432 |
+
tower
|
1433 |
+
stone
|
1434 |
+
forth
|
1435 |
+
gnome
|
1436 |
+
boney
|
1437 |
+
ninth
|
1438 |
+
obese
|
1439 |
+
rotor
|
1440 |
+
mamma
|
1441 |
+
along
|
1442 |
+
knead
|
1443 |
+
ought
|
1444 |
+
unity
|
1445 |
+
foray
|
1446 |
+
phony
|
1447 |
+
clank
|
1448 |
+
gecko
|
1449 |
+
quick
|
1450 |
+
cynic
|
1451 |
+
silly
|
1452 |
+
riser
|
1453 |
+
spire
|
1454 |
+
irate
|
1455 |
+
manic
|
1456 |
+
groan
|
1457 |
+
noose
|
1458 |
+
badge
|
1459 |
+
spite
|
1460 |
+
plush
|
1461 |
+
topic
|
1462 |
+
chasm
|
1463 |
+
slide
|
1464 |
+
cabby
|
1465 |
+
tweak
|
1466 |
+
optic
|
1467 |
+
genie
|
1468 |
+
grove
|
1469 |
+
wryly
|
1470 |
+
march
|
1471 |
+
refer
|
1472 |
+
caulk
|
1473 |
+
seedy
|
1474 |
+
bagel
|
1475 |
+
scrub
|
1476 |
+
ready
|
1477 |
+
quack
|
1478 |
+
catty
|
1479 |
+
hussy
|
1480 |
+
cower
|
1481 |
+
wound
|
1482 |
+
clink
|
1483 |
+
child
|
1484 |
+
crass
|
1485 |
+
slink
|
1486 |
+
float
|
1487 |
+
ample
|
1488 |
+
spray
|
1489 |
+
geeky
|
1490 |
+
banjo
|
1491 |
+
loopy
|
1492 |
+
glade
|
1493 |
+
chill
|
1494 |
+
lilac
|
1495 |
+
stand
|
1496 |
+
kayak
|
1497 |
+
warty
|
1498 |
+
shoot
|
1499 |
+
print
|
1500 |
+
bezel
|
1501 |
+
cream
|
1502 |
+
inter
|
1503 |
+
goody
|
1504 |
+
sooty
|
1505 |
+
clack
|
1506 |
+
saucy
|
1507 |
+
matey
|
1508 |
+
title
|
1509 |
+
spoil
|
1510 |
+
stout
|
1511 |
+
canny
|
1512 |
+
prude
|
1513 |
+
crazy
|
1514 |
+
widow
|
1515 |
+
picky
|
1516 |
+
truth
|
1517 |
+
leapt
|
1518 |
+
rouge
|
1519 |
+
melon
|
1520 |
+
vivid
|
1521 |
+
every
|
1522 |
+
acute
|
1523 |
+
quiet
|
1524 |
+
wider
|
1525 |
+
spine
|
1526 |
+
vodka
|
1527 |
+
small
|
1528 |
+
sully
|
1529 |
+
bread
|
1530 |
+
graze
|
1531 |
+
mauve
|
1532 |
+
larva
|
1533 |
+
palsy
|
1534 |
+
globe
|
1535 |
+
bland
|
1536 |
+
coral
|
1537 |
+
super
|
1538 |
+
befit
|
1539 |
+
homer
|
1540 |
+
flume
|
1541 |
+
quail
|
1542 |
+
novel
|
1543 |
+
chore
|
1544 |
+
crone
|
1545 |
+
coach
|
1546 |
+
ashen
|
1547 |
+
tenet
|
1548 |
+
grime
|
1549 |
+
steep
|
1550 |
+
truss
|
1551 |
+
macro
|
1552 |
+
snore
|
1553 |
+
queue
|
1554 |
+
lurch
|
1555 |
+
twang
|
1556 |
+
comma
|
1557 |
+
focus
|
1558 |
+
skate
|
1559 |
+
black
|
1560 |
+
token
|
1561 |
+
zebra
|
1562 |
+
drink
|
1563 |
+
eager
|
1564 |
+
gruff
|
1565 |
+
cleat
|
1566 |
+
apnea
|
1567 |
+
dream
|
1568 |
+
tepee
|
1569 |
+
habit
|
1570 |
+
fling
|
1571 |
+
tweet
|
1572 |
+
leash
|
1573 |
+
snort
|
1574 |
+
house
|
1575 |
+
donor
|
1576 |
+
hover
|
1577 |
+
haunt
|
1578 |
+
smash
|
1579 |
+
infer
|
1580 |
+
hydro
|
1581 |
+
swing
|
1582 |
+
fifth
|
1583 |
+
eclat
|
1584 |
+
reedy
|
1585 |
+
onion
|
1586 |
+
shush
|
1587 |
+
giant
|
1588 |
+
bongo
|
1589 |
+
cling
|
1590 |
+
cyber
|
1591 |
+
flesh
|
1592 |
+
dingo
|
1593 |
+
stain
|
1594 |
+
pearl
|
1595 |
+
vixen
|
1596 |
+
bilge
|
1597 |
+
crown
|
1598 |
+
prone
|
1599 |
+
tribe
|
1600 |
+
flood
|
1601 |
+
taunt
|
1602 |
+
dummy
|
1603 |
+
weary
|
1604 |
+
ethic
|
1605 |
+
award
|
1606 |
+
nomad
|
1607 |
+
flair
|
1608 |
+
ghost
|
1609 |
+
pilot
|
1610 |
+
beset
|
1611 |
+
drift
|
1612 |
+
crept
|
1613 |
+
break
|
1614 |
+
silky
|
1615 |
+
breed
|
1616 |
+
abort
|
1617 |
+
twine
|
1618 |
+
trash
|
1619 |
+
towel
|
1620 |
+
foggy
|
1621 |
+
medic
|
1622 |
+
blank
|
1623 |
+
wrack
|
1624 |
+
scoop
|
1625 |
+
croak
|
1626 |
+
farce
|
1627 |
+
shrew
|
1628 |
+
locus
|
1629 |
+
lofty
|
1630 |
+
smart
|
1631 |
+
truly
|
1632 |
+
tally
|
1633 |
+
sharp
|
1634 |
+
ennui
|
1635 |
+
demon
|
1636 |
+
growl
|
1637 |
+
stank
|
1638 |
+
clung
|
1639 |
+
regal
|
1640 |
+
alloy
|
1641 |
+
femur
|
1642 |
+
spank
|
1643 |
+
doubt
|
1644 |
+
sober
|
1645 |
+
filmy
|
1646 |
+
spent
|
1647 |
+
daily
|
1648 |
+
charm
|
1649 |
+
guild
|
1650 |
+
touch
|
1651 |
+
nymph
|
1652 |
+
vapid
|
1653 |
+
inept
|
1654 |
+
piggy
|
1655 |
+
arson
|
1656 |
+
llama
|
1657 |
+
lemur
|
1658 |
+
stair
|
1659 |
+
photo
|
1660 |
+
phone
|
1661 |
+
young
|
1662 |
+
deign
|
1663 |
+
those
|
1664 |
+
light
|
1665 |
+
stock
|
1666 |
+
chaff
|
1667 |
+
usual
|
1668 |
+
tense
|
1669 |
+
hyper
|
1670 |
+
gusty
|
1671 |
+
slung
|
1672 |
+
buxom
|
1673 |
+
tawny
|
1674 |
+
bacon
|
1675 |
+
dried
|
1676 |
+
worth
|
1677 |
+
basis
|
1678 |
+
using
|
1679 |
+
speak
|
1680 |
+
penne
|
1681 |
+
scree
|
1682 |
+
willy
|
1683 |
+
visit
|
1684 |
+
raise
|
1685 |
+
hoist
|
1686 |
+
clone
|
1687 |
+
utile
|
1688 |
+
nurse
|
1689 |
+
usher
|
1690 |
+
plied
|
1691 |
+
ankle
|
1692 |
+
radii
|
1693 |
+
tulle
|
1694 |
+
pasty
|
1695 |
+
lithe
|
1696 |
+
sheen
|
1697 |
+
barge
|
1698 |
+
crick
|
1699 |
+
aside
|
1700 |
+
gaily
|
1701 |
+
kneed
|
1702 |
+
copse
|
1703 |
+
tonga
|
1704 |
+
swoon
|
1705 |
+
bleed
|
1706 |
+
index
|
1707 |
+
basal
|
1708 |
+
shaft
|
1709 |
+
dairy
|
1710 |
+
aback
|
1711 |
+
swirl
|
1712 |
+
scale
|
1713 |
+
inbox
|
1714 |
+
allow
|
1715 |
+
ombre
|
1716 |
+
churn
|
1717 |
+
choke
|
1718 |
+
bulky
|
1719 |
+
ultra
|
1720 |
+
smoky
|
1721 |
+
pygmy
|
1722 |
+
bribe
|
1723 |
+
afoul
|
1724 |
+
fluid
|
1725 |
+
bugle
|
1726 |
+
avail
|
1727 |
+
kappa
|
1728 |
+
borne
|
1729 |
+
orbit
|
1730 |
+
sinew
|
1731 |
+
acorn
|
1732 |
+
envoy
|
1733 |
+
glyph
|
1734 |
+
cameo
|
1735 |
+
three
|
1736 |
+
tithe
|
1737 |
+
cloak
|
1738 |
+
fella
|
1739 |
+
amiss
|
1740 |
+
mount
|
1741 |
+
blare
|
1742 |
+
delta
|
1743 |
+
blush
|
1744 |
+
saint
|
1745 |
+
given
|
1746 |
+
whack
|
1747 |
+
deuce
|
1748 |
+
heath
|
1749 |
+
shift
|
1750 |
+
bayou
|
1751 |
+
stuff
|
1752 |
+
joker
|
1753 |
+
court
|
1754 |
+
pride
|
1755 |
+
plant
|
1756 |
+
pushy
|
1757 |
+
flack
|
1758 |
+
wagon
|
1759 |
+
split
|
1760 |
+
verve
|
1761 |
+
scion
|
1762 |
+
swarm
|
1763 |
+
bulge
|
1764 |
+
mammy
|
1765 |
+
prism
|
1766 |
+
plump
|
1767 |
+
ditty
|
1768 |
+
decry
|
1769 |
+
salon
|
1770 |
+
shack
|
1771 |
+
woken
|
1772 |
+
align
|
1773 |
+
error
|
1774 |
+
frost
|
1775 |
+
forty
|
1776 |
+
synod
|
1777 |
+
reuse
|
1778 |
+
hobby
|
1779 |
+
nobly
|
1780 |
+
fishy
|
1781 |
+
dusty
|
1782 |
+
clamp
|
1783 |
+
pound
|
1784 |
+
dutch
|
1785 |
+
colon
|
1786 |
+
niece
|
1787 |
+
whiny
|
1788 |
+
bluff
|
1789 |
+
prose
|
1790 |
+
grief
|
1791 |
+
ember
|
1792 |
+
chide
|
1793 |
+
tatty
|
1794 |
+
cider
|
1795 |
+
scald
|
1796 |
+
dread
|
1797 |
+
shave
|
1798 |
+
cagey
|
1799 |
+
petty
|
1800 |
+
egret
|
1801 |
+
witty
|
1802 |
+
golem
|
1803 |
+
anger
|
1804 |
+
idiot
|
1805 |
+
scarf
|
1806 |
+
audio
|
1807 |
+
boost
|
1808 |
+
extra
|
1809 |
+
force
|
1810 |
+
lanky
|
1811 |
+
strip
|
1812 |
+
owner
|
1813 |
+
brush
|
1814 |
+
belly
|
1815 |
+
budge
|
1816 |
+
cause
|
1817 |
+
upper
|
1818 |
+
radio
|
1819 |
+
spree
|
1820 |
+
botch
|
1821 |
+
ether
|
1822 |
+
fetal
|
1823 |
+
gnash
|
1824 |
+
ruder
|
1825 |
+
logic
|
1826 |
+
vaunt
|
1827 |
+
scamp
|
1828 |
+
ovary
|
1829 |
+
musky
|
1830 |
+
spurt
|
1831 |
+
godly
|
1832 |
+
unfed
|
1833 |
+
offal
|
1834 |
+
rhyme
|
1835 |
+
birth
|
1836 |
+
soapy
|
1837 |
+
debar
|
1838 |
+
greet
|
1839 |
+
mealy
|
1840 |
+
solar
|
1841 |
+
await
|
1842 |
+
crack
|
1843 |
+
nylon
|
1844 |
+
mourn
|
1845 |
+
horse
|
1846 |
+
exult
|
1847 |
+
ralph
|
1848 |
+
igloo
|
1849 |
+
twice
|
1850 |
+
ledge
|
1851 |
+
theta
|
1852 |
+
login
|
1853 |
+
shrug
|
1854 |
+
usurp
|
1855 |
+
grown
|
1856 |
+
condo
|
1857 |
+
riper
|
1858 |
+
scout
|
1859 |
+
brief
|
1860 |
+
nosey
|
1861 |
+
excel
|
1862 |
+
welch
|
1863 |
+
shaky
|
1864 |
+
visor
|
1865 |
+
patch
|
1866 |
+
swamp
|
1867 |
+
bible
|
1868 |
+
fresh
|
1869 |
+
beefy
|
1870 |
+
alike
|
1871 |
+
stale
|
1872 |
+
share
|
1873 |
+
acrid
|
1874 |
+
adept
|
1875 |
+
straw
|
1876 |
+
credo
|
1877 |
+
needy
|
1878 |
+
mover
|
1879 |
+
bobby
|
1880 |
+
witch
|
1881 |
+
showy
|
1882 |
+
broke
|
1883 |
+
whelp
|
1884 |
+
trade
|
1885 |
+
dowdy
|
1886 |
+
junta
|
1887 |
+
range
|
1888 |
+
staff
|
1889 |
+
spurn
|
1890 |
+
olive
|
1891 |
+
miner
|
1892 |
+
tiara
|
1893 |
+
axion
|
1894 |
+
caddy
|
1895 |
+
bonus
|
1896 |
+
valve
|
1897 |
+
grimy
|
1898 |
+
bigot
|
1899 |
+
broil
|
1900 |
+
honor
|
1901 |
+
drama
|
1902 |
+
donut
|
1903 |
+
stuck
|
1904 |
+
pasta
|
1905 |
+
loser
|
1906 |
+
mange
|
1907 |
+
annex
|
1908 |
+
lousy
|
1909 |
+
spook
|
1910 |
+
hippy
|
1911 |
+
alter
|
1912 |
+
usage
|
1913 |
+
daunt
|
1914 |
+
alien
|
1915 |
+
trite
|
1916 |
+
shunt
|
1917 |
+
aorta
|
1918 |
+
tramp
|
1919 |
+
psalm
|
1920 |
+
essay
|
1921 |
+
steal
|
1922 |
+
sneer
|
1923 |
+
spore
|
1924 |
+
elude
|
1925 |
+
avert
|
1926 |
+
frisk
|
1927 |
+
spade
|
1928 |
+
knock
|
1929 |
+
prove
|
1930 |
+
story
|
1931 |
+
singe
|
1932 |
+
speed
|
1933 |
+
gassy
|
1934 |
+
odder
|
1935 |
+
berry
|
1936 |
+
buggy
|
1937 |
+
ninja
|
1938 |
+
leave
|
1939 |
+
shook
|
1940 |
+
dicey
|
1941 |
+
harem
|
1942 |
+
gloat
|
1943 |
+
manly
|
1944 |
+
booze
|
1945 |
+
waste
|
1946 |
+
rough
|
1947 |
+
flout
|
1948 |
+
false
|
1949 |
+
mogul
|
1950 |
+
modem
|
1951 |
+
stick
|
1952 |
+
ingot
|
1953 |
+
extol
|
1954 |
+
guava
|
1955 |
+
blast
|
1956 |
+
hazel
|
1957 |
+
erupt
|
1958 |
+
shalt
|
1959 |
+
swell
|
1960 |
+
brunt
|
1961 |
+
alive
|
1962 |
+
brick
|
1963 |
+
stall
|
1964 |
+
aroma
|
1965 |
+
abide
|
1966 |
+
musty
|
1967 |
+
beech
|
1968 |
+
slyly
|
1969 |
+
theft
|
1970 |
+
latch
|
1971 |
+
aisle
|
1972 |
+
papal
|
1973 |
+
gully
|
1974 |
+
knack
|
1975 |
+
match
|
1976 |
+
trawl
|
1977 |
+
urine
|
1978 |
+
rouse
|
1979 |
+
endow
|
1980 |
+
ruler
|
1981 |
+
blown
|
1982 |
+
angry
|
1983 |
+
lorry
|
1984 |
+
heady
|
1985 |
+
toxin
|
1986 |
+
aglow
|
1987 |
+
friar
|
1988 |
+
begat
|
1989 |
+
khaki
|
1990 |
+
rumor
|
1991 |
+
dodgy
|
1992 |
+
clerk
|
1993 |
+
adore
|
1994 |
+
crock
|
1995 |
+
where
|
1996 |
+
teary
|
1997 |
+
toast
|
1998 |
+
modal
|
1999 |
+
queer
|
2000 |
+
flask
|
2001 |
+
abase
|
2002 |
+
depot
|
2003 |
+
sissy
|
2004 |
+
empty
|
2005 |
+
cloud
|
2006 |
+
dress
|
2007 |
+
owing
|
2008 |
+
avian
|
2009 |
+
enact
|
2010 |
+
razor
|
2011 |
+
today
|
2012 |
+
thing
|
2013 |
+
agate
|
2014 |
+
gripe
|
2015 |
+
creak
|
2016 |
+
waist
|
2017 |
+
crook
|
2018 |
+
annul
|
2019 |
+
etude
|
2020 |
+
opera
|
2021 |
+
savvy
|
2022 |
+
draft
|
2023 |
+
setup
|
2024 |
+
sower
|
2025 |
+
voice
|
2026 |
+
erase
|
2027 |
+
screw
|
2028 |
+
image
|
2029 |
+
smirk
|
2030 |
+
chest
|
2031 |
+
bingo
|
2032 |
+
conch
|
2033 |
+
tweed
|
2034 |
+
howdy
|
2035 |
+
metro
|
2036 |
+
minor
|
2037 |
+
amity
|
2038 |
+
label
|
2039 |
+
atoll
|
2040 |
+
fecal
|
2041 |
+
prior
|
2042 |
+
penal
|
2043 |
+
whoop
|
2044 |
+
duvet
|
2045 |
+
caput
|
2046 |
+
waxen
|
2047 |
+
medal
|
2048 |
+
plaza
|
2049 |
+
chunk
|
2050 |
+
snowy
|
2051 |
+
flung
|
2052 |
+
afoot
|
2053 |
+
mouth
|
2054 |
+
bound
|
2055 |
+
think
|
2056 |
+
gruel
|
2057 |
+
trice
|
2058 |
+
girth
|
2059 |
+
axiom
|
2060 |
+
forgo
|
2061 |
+
yearn
|
2062 |
+
fritz
|
2063 |
+
lapse
|
2064 |
+
basin
|
2065 |
+
rehab
|
2066 |
+
woman
|
2067 |
+
glide
|
2068 |
+
never
|
2069 |
+
about
|
2070 |
+
laugh
|
2071 |
+
grope
|
2072 |
+
grate
|
2073 |
+
beady
|
2074 |
+
unlit
|
2075 |
+
moult
|
2076 |
+
faint
|
2077 |
+
shiny
|
2078 |
+
tacit
|
2079 |
+
groin
|
2080 |
+
chute
|
2081 |
+
expel
|
2082 |
+
hovel
|
2083 |
+
hippo
|
2084 |
+
argue
|
2085 |
+
pecan
|
2086 |
+
folio
|
2087 |
+
ivory
|
2088 |
+
local
|
2089 |
+
sauna
|
2090 |
+
undid
|
2091 |
+
prong
|
2092 |
+
rigid
|
2093 |
+
floss
|
2094 |
+
parse
|
2095 |
+
untie
|
2096 |
+
billy
|
2097 |
+
lunar
|
2098 |
+
plank
|
2099 |
+
mafia
|
2100 |
+
paddy
|
2101 |
+
peace
|
2102 |
+
quart
|
2103 |
+
stray
|
2104 |
+
pixie
|
2105 |
+
sunny
|
2106 |
+
swore
|
2107 |
+
shirt
|
2108 |
+
chalk
|
2109 |
+
sweat
|
2110 |
+
wordy
|
2111 |
+
older
|
2112 |
+
lower
|
2113 |
+
abyss
|
2114 |
+
urban
|
2115 |
+
steed
|
2116 |
+
mummy
|
2117 |
+
bosom
|
2118 |
+
among
|
2119 |
+
onset
|
2120 |
+
goofy
|
2121 |
+
froth
|
2122 |
+
halve
|
2123 |
+
filly
|
2124 |
+
alley
|
2125 |
+
jaunt
|
2126 |
+
livid
|
2127 |
+
taboo
|
2128 |
+
puree
|
2129 |
+
clock
|
2130 |
+
coven
|
2131 |
+
risky
|
2132 |
+
greed
|
2133 |
+
resin
|
2134 |
+
salty
|
2135 |
+
drape
|
2136 |
+
abhor
|
2137 |
+
level
|
2138 |
+
weedy
|
2139 |
+
allay
|
2140 |
+
meter
|
2141 |
+
crest
|
2142 |
+
limit
|
2143 |
+
stage
|
2144 |
+
thrum
|
2145 |
+
troop
|
2146 |
+
salsa
|
2147 |
+
cacti
|
2148 |
+
sperm
|
2149 |
+
felon
|
2150 |
+
tarot
|
2151 |
+
fewer
|
2152 |
+
niche
|
2153 |
+
rider
|
2154 |
+
pouch
|
2155 |
+
ditch
|
2156 |
+
press
|
2157 |
+
cedar
|
2158 |
+
clash
|
2159 |
+
newly
|
2160 |
+
lowly
|
2161 |
+
buyer
|
2162 |
+
adult
|
2163 |
+
woody
|
2164 |
+
taint
|
2165 |
+
gauze
|
2166 |
+
sleek
|
2167 |
+
angel
|
2168 |
+
triad
|
2169 |
+
frail
|
2170 |
+
stake
|
2171 |
+
spunk
|
2172 |
+
repel
|
2173 |
+
crumb
|
2174 |
+
candy
|
2175 |
+
arrow
|
2176 |
+
troll
|
2177 |
+
worse
|
2178 |
+
patty
|
2179 |
+
nanny
|
2180 |
+
gypsy
|
2181 |
+
creep
|
2182 |
+
guide
|
2183 |
+
power
|
2184 |
+
salvo
|
2185 |
+
glean
|
2186 |
+
racer
|
2187 |
+
bloom
|
2188 |
+
diner
|
2189 |
+
lyric
|
2190 |
+
dwelt
|
2191 |
+
erect
|
2192 |
+
fatty
|
2193 |
+
datum
|
2194 |
+
stink
|
2195 |
+
smote
|
2196 |
+
icily
|
2197 |
+
axial
|
2198 |
+
shyly
|
2199 |
+
golly
|
2200 |
+
itchy
|
2201 |
+
snoop
|
2202 |
+
ovate
|
2203 |
+
missy
|
2204 |
+
muddy
|
2205 |
+
cairn
|
2206 |
+
pouty
|
2207 |
+
panic
|
2208 |
+
crash
|
2209 |
+
femme
|
2210 |
+
decoy
|
2211 |
+
masse
|
2212 |
+
spasm
|
2213 |
+
noise
|
2214 |
+
skirt
|
2215 |
+
oddly
|
2216 |
+
minim
|
2217 |
+
hunch
|
2218 |
+
gawky
|
2219 |
+
crane
|
2220 |
+
steam
|
2221 |
+
tangy
|
2222 |
+
whale
|
2223 |
+
laden
|
2224 |
+
midst
|
2225 |
+
linen
|
2226 |
+
savoy
|
2227 |
+
dealt
|
2228 |
+
soggy
|
2229 |
+
grain
|
2230 |
+
cable
|
2231 |
+
cycle
|
2232 |
+
venom
|
2233 |
+
doing
|
2234 |
+
shark
|
2235 |
+
flirt
|
2236 |
+
stash
|
2237 |
+
squad
|
2238 |
+
elide
|
2239 |
+
realm
|
2240 |
+
cliff
|
2241 |
+
sleep
|
2242 |
+
vista
|
2243 |
+
rapid
|
2244 |
+
badly
|
2245 |
+
shown
|
2246 |
+
pooch
|
2247 |
+
creed
|
2248 |
+
loath
|
2249 |
+
jolly
|
2250 |
+
mocha
|
2251 |
+
briar
|
2252 |
+
cease
|
2253 |
+
arena
|
2254 |
+
unzip
|
2255 |
+
elbow
|
2256 |
+
gaunt
|
2257 |
+
teeth
|
2258 |
+
poesy
|
2259 |
+
clown
|
2260 |
+
prawn
|
2261 |
+
rower
|
2262 |
+
rigor
|
2263 |
+
ferry
|
2264 |
+
wimpy
|
2265 |
+
splat
|
2266 |
+
skill
|
2267 |
+
patsy
|
2268 |
+
agape
|
2269 |
+
puffy
|
2270 |
+
exert
|
2271 |
+
sting
|
2272 |
+
lipid
|
2273 |
+
ardor
|
2274 |
+
afire
|
2275 |
+
slope
|
2276 |
+
clove
|
2277 |
+
aware
|
2278 |
+
gayer
|
2279 |
+
grant
|
2280 |
+
magma
|
2281 |
+
purse
|
2282 |
+
glass
|
2283 |
+
dirge
|
2284 |
+
water
|
2285 |
+
renew
|
2286 |
+
count
|
2287 |
+
funky
|
2288 |
+
bring
|
2289 |
+
scorn
|
2290 |
+
lease
|
2291 |
+
spilt
|
2292 |
+
stein
|
2293 |
+
wooly
|
2294 |
+
plunk
|
2295 |
+
cargo
|
2296 |
+
carry
|
2297 |
+
rabid
|
2298 |
+
mouse
|
2299 |
+
wight
|
2300 |
+
event
|
2301 |
+
favor
|
2302 |
+
maple
|
2303 |
+
debug
|
2304 |
+
trend
|
2305 |
+
twixt
|
2306 |
+
chord
|
2307 |
+
music
|
2308 |
+
guile
|
2309 |
+
table
|
notebooks/__pycache__/wordle_assistant_functions.cpython-310.pyc
ADDED
Binary file (18.9 kB). View file
|
|
notebooks/__pycache__/wordle_functions.cpython-310.pyc
ADDED
Binary file (19 kB). View file
|
|
notebooks/getting_daily_word.ipynb
ADDED
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"cells": [
|
3 |
+
{
|
4 |
+
"cell_type": "markdown",
|
5 |
+
"metadata": {},
|
6 |
+
"source": [
|
7 |
+
"# Next Steps\n",
|
8 |
+
"\n",
|
9 |
+
"- Use Selenium instead (use Allard scraping code)"
|
10 |
+
]
|
11 |
+
},
|
12 |
+
{
|
13 |
+
"cell_type": "markdown",
|
14 |
+
"metadata": {},
|
15 |
+
"source": [
|
16 |
+
"# Selenium\n",
|
17 |
+
"\n",
|
18 |
+
"- Use Selenium to fill out game with current provided guesses and give user feedback on best guess at the moment"
|
19 |
+
]
|
20 |
+
},
|
21 |
+
{
|
22 |
+
"cell_type": "code",
|
23 |
+
"execution_count": 1,
|
24 |
+
"metadata": {},
|
25 |
+
"outputs": [],
|
26 |
+
"source": [
|
27 |
+
"# import requests\n",
|
28 |
+
"from bs4 import BeautifulSoup\n",
|
29 |
+
"\n",
|
30 |
+
"# !pip install selenium\n",
|
31 |
+
"from selenium import webdriver\n",
|
32 |
+
"from selenium.webdriver.common.keys import Keys\n",
|
33 |
+
"from selenium.webdriver.chrome.options import Options\n",
|
34 |
+
"\n",
|
35 |
+
"# Configure ChromeOptions\n",
|
36 |
+
"chrome_options = Options()\n",
|
37 |
+
"chrome_options.add_argument(\"--headless\") # Run Chrome in headless mode"
|
38 |
+
]
|
39 |
+
},
|
40 |
+
{
|
41 |
+
"cell_type": "code",
|
42 |
+
"execution_count": 2,
|
43 |
+
"metadata": {},
|
44 |
+
"outputs": [
|
45 |
+
{
|
46 |
+
"data": {
|
47 |
+
"text/plain": [
|
48 |
+
"'topaz'"
|
49 |
+
]
|
50 |
+
},
|
51 |
+
"execution_count": 2,
|
52 |
+
"metadata": {},
|
53 |
+
"output_type": "execute_result"
|
54 |
+
}
|
55 |
+
],
|
56 |
+
"source": [
|
57 |
+
"# driver = webdriver.Chrome()\n",
|
58 |
+
"driver = webdriver.Chrome(options = chrome_options)\n",
|
59 |
+
"\n",
|
60 |
+
"url = \"https://screenrant.com/wordle-answers-updated-word-puzzle-guide/\"\n",
|
61 |
+
"\n",
|
62 |
+
"# Navigate to the URL\n",
|
63 |
+
"driver.get(url)\n",
|
64 |
+
"\n",
|
65 |
+
"# Get the page source\n",
|
66 |
+
"html_content = driver.page_source\n",
|
67 |
+
" \n",
|
68 |
+
"soup = BeautifulSoup(html_content, \"html.parser\")\n",
|
69 |
+
"for item in soup.find_all('a'):\n",
|
70 |
+
" \n",
|
71 |
+
" item_link = item['href']\n",
|
72 |
+
"\n",
|
73 |
+
" link_prefix = \"https://screenrant.com/todays-wordle-answer-hints\"\n",
|
74 |
+
"\n",
|
75 |
+
" if item_link:\n",
|
76 |
+
" \n",
|
77 |
+
" if link_prefix in item_link:\n",
|
78 |
+
" good_item = item\n",
|
79 |
+
" break\n",
|
80 |
+
"\n",
|
81 |
+
"# print(good_item)\n",
|
82 |
+
"\n",
|
83 |
+
"good_text = good_item.text\n",
|
84 |
+
"\n",
|
85 |
+
"target_word = good_text.split(\" - \")[-1].lower()\n",
|
86 |
+
"target_word"
|
87 |
+
]
|
88 |
+
}
|
89 |
+
],
|
90 |
+
"metadata": {
|
91 |
+
"kernelspec": {
|
92 |
+
"display_name": "base",
|
93 |
+
"language": "python",
|
94 |
+
"name": "python3"
|
95 |
+
},
|
96 |
+
"language_info": {
|
97 |
+
"codemirror_mode": {
|
98 |
+
"name": "ipython",
|
99 |
+
"version": 3
|
100 |
+
},
|
101 |
+
"file_extension": ".py",
|
102 |
+
"mimetype": "text/x-python",
|
103 |
+
"name": "python",
|
104 |
+
"nbconvert_exporter": "python",
|
105 |
+
"pygments_lexer": "ipython3",
|
106 |
+
"version": "3.10.2"
|
107 |
+
},
|
108 |
+
"orig_nbformat": 4
|
109 |
+
},
|
110 |
+
"nbformat": 4,
|
111 |
+
"nbformat_minor": 2
|
112 |
+
}
|
notebooks/wordle_assistant.ipynb
ADDED
@@ -0,0 +1,1392 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"cells": [
|
3 |
+
{
|
4 |
+
"cell_type": "code",
|
5 |
+
"execution_count": 1,
|
6 |
+
"metadata": {},
|
7 |
+
"outputs": [],
|
8 |
+
"source": [
|
9 |
+
"from wordle_assistant_functions import *"
|
10 |
+
]
|
11 |
+
},
|
12 |
+
{
|
13 |
+
"cell_type": "code",
|
14 |
+
"execution_count": 2,
|
15 |
+
"metadata": {},
|
16 |
+
"outputs": [
|
17 |
+
{
|
18 |
+
"name": "stdout",
|
19 |
+
"output_type": "stream",
|
20 |
+
"text": [
|
21 |
+
"2310\n"
|
22 |
+
]
|
23 |
+
},
|
24 |
+
{
|
25 |
+
"data": {
|
26 |
+
"text/plain": [
|
27 |
+
"['wince', 'thyme', 'mower', 'horde', 'heard']"
|
28 |
+
]
|
29 |
+
},
|
30 |
+
"execution_count": 2,
|
31 |
+
"metadata": {},
|
32 |
+
"output_type": "execute_result"
|
33 |
+
}
|
34 |
+
],
|
35 |
+
"source": [
|
36 |
+
"### Official list\n",
|
37 |
+
"official_words = []\n",
|
38 |
+
"\n",
|
39 |
+
"with open(\"../data/official_words_processed.txt\", \"r\", encoding = \"utf-8\") as f:\n",
|
40 |
+
" for word in f.read().split(\"\\n\"):\n",
|
41 |
+
" official_words.append(word)\n",
|
42 |
+
"\n",
|
43 |
+
"f.close() # closes connection to file\n",
|
44 |
+
"\n",
|
45 |
+
"print(len(official_words))\n",
|
46 |
+
"official_words[:5]"
|
47 |
+
]
|
48 |
+
},
|
49 |
+
{
|
50 |
+
"cell_type": "code",
|
51 |
+
"execution_count": 3,
|
52 |
+
"metadata": {},
|
53 |
+
"outputs": [],
|
54 |
+
"source": [
|
55 |
+
"# test_1 = wordle_wizard(word_list = official_words, max_guesses = 6, \n",
|
56 |
+
"# guess = \"paint\", target = \"force\",\n",
|
57 |
+
"# random_guess = False, random_target = False, \n",
|
58 |
+
"# verbose = True, drama = 0, return_stats = False, record = False)"
|
59 |
+
]
|
60 |
+
},
|
61 |
+
{
|
62 |
+
"cell_type": "code",
|
63 |
+
"execution_count": 13,
|
64 |
+
"metadata": {},
|
65 |
+
"outputs": [],
|
66 |
+
"source": [
|
67 |
+
"def wordle_wizard_cheat_local(guesses: list, word_list: list, max_guesses: int = None, \n",
|
68 |
+
" # guess: str = None, target: str = None,\n",
|
69 |
+
" target: str = None,\n",
|
70 |
+
" random_guess: bool = False, random_target: bool = False, \n",
|
71 |
+
" verbose: bool = False, drama: float = None, \n",
|
72 |
+
" return_stats: bool = False, record: bool = False):\n",
|
73 |
+
" \"\"\"\n",
|
74 |
+
" Mimicking the popular web game, this function matches a current word to a target word automatically, in the most statistically optimal way possible.\n",
|
75 |
+
"\n",
|
76 |
+
" Parameters:\n",
|
77 |
+
" ------\n",
|
78 |
+
" `word_list`: list\n",
|
79 |
+
" list of valid words to be considered\n",
|
80 |
+
" `guess`: str\n",
|
81 |
+
" a string -- must be the same length as `target_word`\n",
|
82 |
+
" `target`: str\n",
|
83 |
+
" a string -- must be the same length as `opening_word`\n",
|
84 |
+
" `max_guesses`: int\n",
|
85 |
+
" the maximum number of attempts allowed to solve the Wordle\n",
|
86 |
+
" `random_guess`: bool\n",
|
87 |
+
" if True, randomly chooses a starting word from all words within `word_list`. If False, passed starting word must be used instead\n",
|
88 |
+
" `random_target`: bool\n",
|
89 |
+
" if True, randomly chooses a target word from all words within `word_list`. If False, passed target word must be used instead\n",
|
90 |
+
" `verbose`: bool\n",
|
91 |
+
" if True, # prints progress and explanation of how function solves the puzzle. If False, # prints only the guessed word at each guess.\n",
|
92 |
+
" `drama`: float or int\n",
|
93 |
+
" if int provided, each guess' output is delayed by that number of seconds, else each output is shown as quickly as possible. For ~dRaMaTiC eFfEcT~\n",
|
94 |
+
" `return_stats`: bool\n",
|
95 |
+
" if True, # prints nothing and returns a dictionary of various statistics about the function's performance trying to solve the puzzle\n",
|
96 |
+
" `record`: bool\n",
|
97 |
+
" if True, creates a .txt file with the same information # printed according to the indicated verbosity\n",
|
98 |
+
"\n",
|
99 |
+
" Returns:\n",
|
100 |
+
" ------\n",
|
101 |
+
" `stats_dict`: dict\n",
|
102 |
+
" dictionary containing various statistics about the function's performance trying to solve the puzzle\n",
|
103 |
+
" \"\"\"\n",
|
104 |
+
"\n",
|
105 |
+
" # guess = guess.lower()\n",
|
106 |
+
" target = target.lower()\n",
|
107 |
+
"\n",
|
108 |
+
" if target not in word_list:\n",
|
109 |
+
" word_list.append(target)\n",
|
110 |
+
"\n",
|
111 |
+
" sugg_words = []\n",
|
112 |
+
"\n",
|
113 |
+
" for i in range(0, 20):\n",
|
114 |
+
" ran_int = random.randint(0, len(word_list) - 1)\n",
|
115 |
+
" word = word_list[ran_int]\n",
|
116 |
+
" sugg_words.append(word)\n",
|
117 |
+
"\n",
|
118 |
+
" guess = guesses[0]\n",
|
119 |
+
"\n",
|
120 |
+
" stats_dict = {}\n",
|
121 |
+
" stats_dict['first_guess'] = guess\n",
|
122 |
+
" stats_dict['target_word'] = target\n",
|
123 |
+
" stats_dict['first_guess_vowels'] = float(count_vows_cons(guess, y_vow = True)['vows'])\n",
|
124 |
+
" stats_dict['first_guess_consonants'] = float(count_vows_cons(guess, y_vow = True)['cons'])\n",
|
125 |
+
" stats_dict['target_vowels'] = float(count_vows_cons(target, y_vow = True)['vows'])\n",
|
126 |
+
" stats_dict['target_consonants'] = float(count_vows_cons(target, y_vow = True)['cons'])\n",
|
127 |
+
" \n",
|
128 |
+
" # get rating of the first guess word and target word in the entire word_list\n",
|
129 |
+
" for tup in get_word_rating(word_list, word_list, normalized = True):\n",
|
130 |
+
" if tup[0] == guess:\n",
|
131 |
+
" stats_dict['first_guess_rating'] = tup[1]\n",
|
132 |
+
" if tup[0] == target:\n",
|
133 |
+
" stats_dict['target_rating'] = tup[1]\n",
|
134 |
+
"\n",
|
135 |
+
" guess_entropies = []\n",
|
136 |
+
" guess_entropies.append(stats_dict['first_guess_rating'])\n",
|
137 |
+
"\n",
|
138 |
+
" # luck_guess_1 = round(1 - ((1 / len(word_list)) * guess_entropies[0] / 100), 2) * 100\n",
|
139 |
+
"\n",
|
140 |
+
" english_alphabet = \"abcdefghijklmnopqrstuvwxyz\"\n",
|
141 |
+
"\n",
|
142 |
+
" # word_list_sorted_counts = get_letter_counts(english_alphabet, word_list, sort = \"descending\")\n",
|
143 |
+
" word_list_sorted_counts = get_letter_counts(word_list = word_list, letters = english_alphabet, sort = \"descending\", unique = True)\n",
|
144 |
+
"\n",
|
145 |
+
" wordlen = len(guesses[0])\n",
|
146 |
+
" letter_positions = set(i for i in range(0, wordlen))\n",
|
147 |
+
"\n",
|
148 |
+
" guess_set = set()\n",
|
149 |
+
" perfect_dict = {}\n",
|
150 |
+
" wrong_pos_dict = {}\n",
|
151 |
+
" wrong_pos_set = set()\n",
|
152 |
+
" dont_guess_again = set()\n",
|
153 |
+
"\n",
|
154 |
+
" guessed_words = [] # running set of guessed words\n",
|
155 |
+
" guess_num = 0 # baseline for variable\n",
|
156 |
+
" dont_guess_words = set()\n",
|
157 |
+
" incorrect_positions = []\n",
|
158 |
+
" reduction_per_guess = []\n",
|
159 |
+
"\n",
|
160 |
+
" if max_guesses == None: # if no value is passed, default is len(guess)\n",
|
161 |
+
" max_guesses = wordlen\n",
|
162 |
+
" else: # else it is the value passed\n",
|
163 |
+
" max_guesses = max_guesses\n",
|
164 |
+
"\n",
|
165 |
+
" perfect_letts_per_guess = []\n",
|
166 |
+
" wrong_pos_per_guess = []\n",
|
167 |
+
" wrong_letts_per_guess = []\n",
|
168 |
+
"\n",
|
169 |
+
" # while guess: # while there is any guess -- there are conditions to break it at the bottom\n",
|
170 |
+
"\n",
|
171 |
+
" for guess_num, guess in enumerate(guesses):\n",
|
172 |
+
"\n",
|
173 |
+
" guess_num += 1\n",
|
174 |
+
"\n",
|
175 |
+
" guessed_words.append(guess)\n",
|
176 |
+
"\n",
|
177 |
+
" if drama:\n",
|
178 |
+
" time.sleep(drama)\n",
|
179 |
+
"\n",
|
180 |
+
" # guess_num += 1 # each time the guess is processed\n",
|
181 |
+
" if return_stats == False:\n",
|
182 |
+
" if guess_num == 1:\n",
|
183 |
+
" print(\"-----------------------------\\n\")\n",
|
184 |
+
"\n",
|
185 |
+
" if guess == target:\n",
|
186 |
+
" stats_dict['target_guessed'] = True\n",
|
187 |
+
" if return_stats == False:\n",
|
188 |
+
" if guess_num == 1:\n",
|
189 |
+
" # print(f\"Congratulations! The Wordle has been solved in {guess_num} guess, that's amazingly lucky!\")\n",
|
190 |
+
" print(f\"The starting word and target word are the same. Try entering two different words to see how the puzzle can be solved.\")\n",
|
191 |
+
" # print(f\"The target word was {target}\")\n",
|
192 |
+
" \n",
|
193 |
+
" \n",
|
194 |
+
" perfect_letts_per_guess.append(5)\n",
|
195 |
+
" wrong_pos_per_guess.append(0)\n",
|
196 |
+
" wrong_letts_per_guess.append(0)\n",
|
197 |
+
" break\n",
|
198 |
+
" \n",
|
199 |
+
" if return_stats == False:\n",
|
200 |
+
" print(f\"**Guess {guess_num}: '{guess}'**\")\n",
|
201 |
+
"\n",
|
202 |
+
" guess_set = set()\n",
|
203 |
+
" wrong_pos_set = set()\n",
|
204 |
+
"\n",
|
205 |
+
" #### Step 2 -- ALL PERFECT\n",
|
206 |
+
" for i in letter_positions: # number of letters in each word (current word and target word)\n",
|
207 |
+
" guess_set.add(guess[i])\n",
|
208 |
+
"\n",
|
209 |
+
" if guess[i] not in perfect_dict:\n",
|
210 |
+
" perfect_dict[guess[i]] = set()\n",
|
211 |
+
" if guess[i] not in wrong_pos_dict:\n",
|
212 |
+
" wrong_pos_dict[guess[i]] = set()\n",
|
213 |
+
"\n",
|
214 |
+
" ### EVALUATE CURRENT GUESS\n",
|
215 |
+
" if guess[i] == target[i]: # letter == correct and position == correct\n",
|
216 |
+
" perfect_dict[guess[i]].add(i)\n",
|
217 |
+
"\n",
|
218 |
+
" if (guess[i] != target[i] and guess[i] in target): # letter == correct and position != correct\n",
|
219 |
+
" wrong_pos_dict[guess[i]].add(i)\n",
|
220 |
+
" wrong_pos_set.add(guess[i])\n",
|
221 |
+
"\n",
|
222 |
+
" if guess[i] not in target: # if letter is not relevant at all\n",
|
223 |
+
" dont_guess_again.add(guess[i])\n",
|
224 |
+
"\n",
|
225 |
+
" #### Step 3 -- ALL PERFECT\n",
|
226 |
+
" next_letters = set()\n",
|
227 |
+
" for letter, positions in perfect_dict.items():\n",
|
228 |
+
" if len(positions) > 0:\n",
|
229 |
+
" next_letters.add(letter)\n",
|
230 |
+
"\n",
|
231 |
+
" for letter, positions in wrong_pos_dict.items():\n",
|
232 |
+
" if len(positions) > 0:\n",
|
233 |
+
" next_letters.add(letter)\n",
|
234 |
+
"\n",
|
235 |
+
" #### List of tuples of correct letter positions in new valid words. Eg: [('e', 2), ('a', 3)]\n",
|
236 |
+
" perfect_letters = []\n",
|
237 |
+
" for letter, positions in perfect_dict.items():\n",
|
238 |
+
" for pos in positions:\n",
|
239 |
+
" if len(positions) > 0:\n",
|
240 |
+
" perfect_letters.append((letter, pos))\n",
|
241 |
+
"\n",
|
242 |
+
" #### all words that have correct letters in same spots\n",
|
243 |
+
" words_matching_correct_all = []\n",
|
244 |
+
" for word in word_list:\n",
|
245 |
+
" word_set = set()\n",
|
246 |
+
" for letter, pos in perfect_letters:\n",
|
247 |
+
" if pos < len(word):\n",
|
248 |
+
" if word[pos] == letter:\n",
|
249 |
+
" words_matching_correct_all.append(word)\n",
|
250 |
+
"\n",
|
251 |
+
" #### excluding words with letters in known incorrect positions\n",
|
252 |
+
" for letter, positions in wrong_pos_dict.items():\n",
|
253 |
+
" for pos in positions:\n",
|
254 |
+
" if len(positions) > 0:\n",
|
255 |
+
" if (letter, pos) not in incorrect_positions:\n",
|
256 |
+
" incorrect_positions.append((letter, pos))\n",
|
257 |
+
"\n",
|
258 |
+
" # sorting lists of tuples just to make them look nice in the # printout\n",
|
259 |
+
" incorrect_positions = sorted(incorrect_positions, key = operator.itemgetter(1), reverse = False)\n",
|
260 |
+
" perfect_letters = sorted(perfect_letters, key = operator.itemgetter(1), reverse = False)\n",
|
261 |
+
"\n",
|
262 |
+
" #### all words that have correct letters in incorrect spots -- so they can be excluded efficiently\n",
|
263 |
+
" \n",
|
264 |
+
" # print(incorrect_positions)\n",
|
265 |
+
" \n",
|
266 |
+
" for word in word_list:\n",
|
267 |
+
" word_set = set()\n",
|
268 |
+
" for letter, pos in incorrect_positions:\n",
|
269 |
+
" if pos < len(word):\n",
|
270 |
+
" if word[pos] == letter:\n",
|
271 |
+
" dont_guess_words.add(word)\n",
|
272 |
+
" for word in word_list:\n",
|
273 |
+
" word_set = set()\n",
|
274 |
+
" for letter, pos in incorrect_positions:\n",
|
275 |
+
" if pos < len(word):\n",
|
276 |
+
" if word[pos] == letter:\n",
|
277 |
+
" dont_guess_words.add(word)\n",
|
278 |
+
"\n",
|
279 |
+
" for bad_letter in dont_guess_again:\n",
|
280 |
+
" for word in word_list:\n",
|
281 |
+
" if (bad_letter in word and word not in dont_guess_words):\n",
|
282 |
+
" dont_guess_words.add(word)\n",
|
283 |
+
"\n",
|
284 |
+
" if return_stats == False:\n",
|
285 |
+
" if verbose == True:\n",
|
286 |
+
" print(f\"Letters in correct positions:\\n\\t{perfect_letters}\\n\")\n",
|
287 |
+
" print(f\"Letters in incorrect positions:\\n\\t{incorrect_positions}\\n\")\n",
|
288 |
+
" # print (f\"Letters to guess again:\\n\\t{sorted(list(next_letters), reverse = False)}\\n\")\n",
|
289 |
+
" print(f\"Letters to not guess again:\\n\\t{sorted(list(dont_guess_again), reverse = False)}\\n\") # works\n",
|
290 |
+
"\n",
|
291 |
+
" # Returns True\n",
|
292 |
+
" # print(A.issubset(B)) # \"if everything in A is in B\", returns Bool\n",
|
293 |
+
"\n",
|
294 |
+
" perfect_letts_per_guess.append(len(perfect_letters))\n",
|
295 |
+
" wrong_pos_per_guess.append(len(incorrect_positions))\n",
|
296 |
+
" wrong_letts_per_guess.append(len(dont_guess_again))\n",
|
297 |
+
"\n",
|
298 |
+
" potential_next_guesses = set()\n",
|
299 |
+
" middle_set = set()\n",
|
300 |
+
"\n",
|
301 |
+
" if len(perfect_letters) == 0 and len(incorrect_positions) == 0: # if there are NEITHER perfect letters, NOR incorrect positions, ....\n",
|
302 |
+
" for word in word_list:\n",
|
303 |
+
" if word not in dont_guess_words:\n",
|
304 |
+
" if word not in guessed_words:\n",
|
305 |
+
" potential_next_guesses.add(word)\n",
|
306 |
+
" \n",
|
307 |
+
" # print(f\"GUESS {guess_num} : TEST 1-1\")\n",
|
308 |
+
"\n",
|
309 |
+
" if len(perfect_letters) == 0 and len(incorrect_positions) != 0: # if there are no perfect letters whatsoever, but there ARE incorrect positions ....\n",
|
310 |
+
" for word in word_list:\n",
|
311 |
+
" for incor_letter, incor_pos in incorrect_positions:\n",
|
312 |
+
" if incor_pos < len(word):\n",
|
313 |
+
" if word[incor_pos] != incor_letter:\n",
|
314 |
+
" if word not in dont_guess_words: # just in case\n",
|
315 |
+
" word_set = set()\n",
|
316 |
+
" for letter in word:\n",
|
317 |
+
" word_set.add(letter)\n",
|
318 |
+
" \n",
|
319 |
+
" if next_letters.issubset(word_set):\n",
|
320 |
+
" if word not in guessed_words:\n",
|
321 |
+
" if len(dont_guess_again) > 0:\n",
|
322 |
+
" for bad_letter in dont_guess_again:\n",
|
323 |
+
" if bad_letter not in word:\n",
|
324 |
+
" # potential_next_guesses.append(word)\n",
|
325 |
+
" potential_next_guesses.add(word)\n",
|
326 |
+
" else:\n",
|
327 |
+
" potential_next_guesses.add(word)\n",
|
328 |
+
" \n",
|
329 |
+
" # print(f\"GUESS {guess_num} : TEST 2-1\")\n",
|
330 |
+
"\n",
|
331 |
+
" else:\n",
|
332 |
+
" for word in word_list:\n",
|
333 |
+
" if word not in dont_guess_words: # just in case\n",
|
334 |
+
" word_set = set()\n",
|
335 |
+
" for letter in word:\n",
|
336 |
+
" word_set.add(letter)\n",
|
337 |
+
" if next_letters.issubset(word_set):\n",
|
338 |
+
" if word not in guessed_words:\n",
|
339 |
+
" # # print (\"TEST 3-2\")\n",
|
340 |
+
"\n",
|
341 |
+
" if len(dont_guess_again) > 0:\n",
|
342 |
+
" for bad_letter in dont_guess_again:\n",
|
343 |
+
" if bad_letter not in word:\n",
|
344 |
+
" middle_set.add(word)\n",
|
345 |
+
" else:\n",
|
346 |
+
" middle_set.add(word)\n",
|
347 |
+
" for word in middle_set:\n",
|
348 |
+
" dummy_list = []\n",
|
349 |
+
" for good_lett, good_pos in perfect_letters:\n",
|
350 |
+
" if word[good_pos] == good_lett:\n",
|
351 |
+
" dummy_list.append(1)\n",
|
352 |
+
" if len(dummy_list) == len(perfect_letters):\n",
|
353 |
+
" potential_next_guesses.add(word)\n",
|
354 |
+
" for word in middle_set:\n",
|
355 |
+
" dummy_list = []\n",
|
356 |
+
" for bad_lett, bad_pos in incorrect_positions:\n",
|
357 |
+
" if bad_pos < len(word):\n",
|
358 |
+
" if word[bad_pos] == bad_lett:\n",
|
359 |
+
" dummy_list.append(1)\n",
|
360 |
+
" if len(dummy_list) > 0:\n",
|
361 |
+
" potential_next_guesses.remove(word)\n",
|
362 |
+
" \n",
|
363 |
+
" # print(f\"GUESS {guess_num} : TEST 3-1\")\n",
|
364 |
+
"\n",
|
365 |
+
" if return_stats == False:\n",
|
366 |
+
" if verbose == True:\n",
|
367 |
+
" if len(potential_next_guesses) > 1:\n",
|
368 |
+
" # print(f\"At this point:\")\n",
|
369 |
+
" print(f\"\\t{len(word_list) - len(potential_next_guesses)}, {round((len(word_list) - len(potential_next_guesses)) / len(word_list) * 100, 2)}% of total words have been eliminated, and\")\n",
|
370 |
+
" print(f\"\\t{len(potential_next_guesses)}, {round(len(potential_next_guesses) / len(word_list) * 100, 2)}% of total words remain possible.\\n\")\n",
|
371 |
+
" \n",
|
372 |
+
" else:\n",
|
373 |
+
" # print(f\"At this point:\")\n",
|
374 |
+
" print(f\"\\t{len(word_list) - len(potential_next_guesses)}, {round((len(word_list) - len(potential_next_guesses)) / len(word_list) * 100, 2)}% of total words have been eliminated, and\")\n",
|
375 |
+
" print(f\"\\t{len(potential_next_guesses)}, {round(len(potential_next_guesses) / len(word_list) * 100, 2)}% of total words remain possible.\\n\")\n",
|
376 |
+
" \n",
|
377 |
+
" reduction_per_guess.append(len(potential_next_guesses))\n",
|
378 |
+
" \n",
|
379 |
+
" #### Guessing next word\n",
|
380 |
+
" if len(potential_next_guesses) == 1:\n",
|
381 |
+
"\n",
|
382 |
+
" if return_stats == False:\n",
|
383 |
+
" if verbose == True:\n",
|
384 |
+
" print(f\"All potential next guesses:\\n\\t{get_word_rating(words_to_rate = list(potential_next_guesses), word_list = word_list)}\\n\")\n",
|
385 |
+
" print(f\"Words guessed so far:\\n\\t{guessed_words}.\\n\")\n",
|
386 |
+
" \n",
|
387 |
+
" print(f\"The only remaining possible word is:\\n\\t'{list(potential_next_guesses)[0]}'\")\n",
|
388 |
+
" \n",
|
389 |
+
" # guess = list(potential_next_guesses)[0]\n",
|
390 |
+
" if guess_num < len(guesses):\n",
|
391 |
+
" guess = guesses[guess_num]\n",
|
392 |
+
" guess_entropies.append(get_word_rating([guess], word_list, normalized = False, ascending = False)[0][1])\n",
|
393 |
+
"\n",
|
394 |
+
" else:\n",
|
395 |
+
"\n",
|
396 |
+
" best_next_guesses = list(potential_next_guesses) \n",
|
397 |
+
" # # print (best_next_guesses)\n",
|
398 |
+
" word_ratings = get_word_rating(best_next_guesses, word_list, normalized = False, ascending = False) # \"internal\" ratings\n",
|
399 |
+
" \n",
|
400 |
+
" # Get max rating of all words\n",
|
401 |
+
" max_rating = -np.inf\n",
|
402 |
+
" for word, rating in word_ratings:\n",
|
403 |
+
" if rating > max_rating:\n",
|
404 |
+
" max_rating = rating\n",
|
405 |
+
"\n",
|
406 |
+
" # add best rated words (all equally best rating in next guess list) to set\n",
|
407 |
+
" best_of_the_best_1 = []\n",
|
408 |
+
" for word, rating in word_ratings:\n",
|
409 |
+
" if rating == max_rating:\n",
|
410 |
+
" best_of_the_best_1.append(word)\n",
|
411 |
+
"\n",
|
412 |
+
" # only using top ten most frequent prefixes suffixes to bias. After that it the impact is especially negligible\n",
|
413 |
+
" test_starts = get_gram_freq(word_list = word_list, letters_length = 1, position = \"start\", search = None)[:10]\n",
|
414 |
+
" test_ends = get_gram_freq(word_list = word_list, letters_length = 1, position = \"end\", search = None)[:10]\n",
|
415 |
+
"\n",
|
416 |
+
" # list of the best words that also have the most frequent starting and ending letters (suffixes and prefixes didn't have an impact)\n",
|
417 |
+
" best_of_the_best_2 = []\n",
|
418 |
+
" for start_gram, start_count in test_starts:\n",
|
419 |
+
" for end_gram, end_count in test_ends:\n",
|
420 |
+
" for word in best_of_the_best_1:\n",
|
421 |
+
" if word[:1] == start_gram and word[-1:] == end_gram:\n",
|
422 |
+
" best_of_the_best_2.append(word)\n",
|
423 |
+
"\n",
|
424 |
+
" # if len(best_of_the_best_2) > 0:\n",
|
425 |
+
" # guess = best_of_the_best_2[0]\n",
|
426 |
+
" # else:\n",
|
427 |
+
" # guess = best_of_the_best_1[0] # they're all equally the best of the best possible guesses so just pick the first\n",
|
428 |
+
"\n",
|
429 |
+
" if guess_num < len(guesses):\n",
|
430 |
+
" guess = guesses[guess_num]\n",
|
431 |
+
" \n",
|
432 |
+
" # guess_entropies.append(get_word_rating([guess], word_list, normalized = False, ascending = False)[0][1])\n",
|
433 |
+
"\n",
|
434 |
+
" if return_stats == False:\n",
|
435 |
+
" if verbose == True:\n",
|
436 |
+
" if len(word_ratings) <= 40:\n",
|
437 |
+
" print(f\"All potential next guesses:\\n\\t{word_ratings}\\n\")\n",
|
438 |
+
" print(f\"Words guessed so far:\\n\\t{guessed_words}.\\n\")\n",
|
439 |
+
" else:\n",
|
440 |
+
" print(f\"The top 40 potential next guesses are:\\n\\t{word_ratings[:40]}\\n\")\n",
|
441 |
+
" print(f\"Words guessed so far:\\n\\t{guessed_words}.\\n\")\n",
|
442 |
+
"\n",
|
443 |
+
" guess_entropies.append(get_word_rating([guess], word_list, normalized = False, ascending = False)[0][1])\n",
|
444 |
+
"\n",
|
445 |
+
" #### Guess has now been made -- what to do next\n",
|
446 |
+
" if guess_num == max_guesses: # if at max guesses allowed\n",
|
447 |
+
" guessed_words.append(guess)\n",
|
448 |
+
" stats_dict['target_guessed'] = False\n",
|
449 |
+
" if return_stats == False:\n",
|
450 |
+
" if verbose == True:\n",
|
451 |
+
" print(\"-----------------------------\\n\")\n",
|
452 |
+
" print(f\"\\nUnfortunately, the puzzle was not solved in {max_guesses} guesses. Better luck next time!\")\n",
|
453 |
+
" print(f\"The target word was '{target}'.\\n\")\n",
|
454 |
+
" print(\"-----------------------------\\n\")\n",
|
455 |
+
" else:\n",
|
456 |
+
" print(f\"\\nUnfortunately, the puzzle was not solved in {max_guesses} guesses. Better luck next time!\")\n",
|
457 |
+
" print(f\"The target word was '{target}'.\\n\")\n",
|
458 |
+
" break\n",
|
459 |
+
" else: # if not at max guesses yet allowed\n",
|
460 |
+
" # stats_dict['target_guessed'] = False\n",
|
461 |
+
" if return_stats == False:\n",
|
462 |
+
" if verbose == True:\n",
|
463 |
+
" if len(potential_next_guesses) > 1:\n",
|
464 |
+
" print(f\"Recommended next guess:\\n\\t'{word_ratings[0][0]}'\")\n",
|
465 |
+
" \n",
|
466 |
+
" # print(f\"Next guess:\\n\\t'{guess}'\")\n",
|
467 |
+
" print(\"\\n-----------------------------\\n\")\n",
|
468 |
+
"\n",
|
469 |
+
" if guess == target:\n",
|
470 |
+
" guess_num += 1\n",
|
471 |
+
" guessed_words.append(guess)\n",
|
472 |
+
" stats_dict['target_guessed'] = True\n",
|
473 |
+
"\n",
|
474 |
+
" if return_stats == False:\n",
|
475 |
+
" print(f\"**Guess {guess_num}: '{guess}'**\\n\")\n",
|
476 |
+
" print(f\"You solved the puzzle in {guess_num} guesses!\")\n",
|
477 |
+
"\n",
|
478 |
+
" if max_guesses - guess_num == 1:\n",
|
479 |
+
" print(f\"There was only {max_guesses - guess_num} guess remaining.\")\n",
|
480 |
+
" else:\n",
|
481 |
+
" print(f\"There were still {max_guesses - guess_num} guesses remaining.\")\n",
|
482 |
+
"\n",
|
483 |
+
" if return_stats == False: \n",
|
484 |
+
" # stats_dict['target_guessed'] = True \n",
|
485 |
+
" print(f\"\\nThe target word was **'{target}'**.\")\n",
|
486 |
+
" print(\"\\n-----------------------------\")\n",
|
487 |
+
" break\n",
|
488 |
+
"\n",
|
489 |
+
" #### STATS STUFF \n",
|
490 |
+
" mid_guesses_vows = 0\n",
|
491 |
+
" mid_guesses_cons = 0\n",
|
492 |
+
" avg_perf_letters = 0\n",
|
493 |
+
" avg_wrong_pos_letters = 0\n",
|
494 |
+
" avg_wrong_letters = 0\n",
|
495 |
+
"\n",
|
496 |
+
" for i, word in enumerate(guessed_words):\n",
|
497 |
+
" mid_guesses_vows += count_vows_cons(word, y_vow = True)['vows']\n",
|
498 |
+
" mid_guesses_cons += count_vows_cons(word, y_vow = True)['cons']\n",
|
499 |
+
" \n",
|
500 |
+
" for i in range(0, len(guessed_words) - 1):\n",
|
501 |
+
" avg_perf_letters += perfect_letts_per_guess[i]\n",
|
502 |
+
" avg_wrong_pos_letters += wrong_pos_per_guess[i]\n",
|
503 |
+
" avg_wrong_letters += wrong_letts_per_guess[i]\n",
|
504 |
+
"\n",
|
505 |
+
" stats_dict['mid_guesses_avg_vows'] = float(round(mid_guesses_vows / len(guessed_words), 2))\n",
|
506 |
+
" stats_dict['mid_guesses_avg_cons'] = float(round(mid_guesses_cons / len(guessed_words), 2))\n",
|
507 |
+
"\n",
|
508 |
+
" stats_dict['avg_perf_letters'] = float(round(np.mean(avg_perf_letters), 2))\n",
|
509 |
+
" stats_dict['avg_wrong_pos_letters'] = float(round(np.mean(avg_wrong_pos_letters), 2))\n",
|
510 |
+
" stats_dict['avg_wrong_letters'] = float(round(np.mean(avg_wrong_letters), 2))\n",
|
511 |
+
" \n",
|
512 |
+
" # average number of words remaining after each guess -- the higher this is, the luckier the person got (the lower, the more guesses it took)\n",
|
513 |
+
" stats_dict['avg_remaining'] = float(round(np.mean(reduction_per_guess), 2))\n",
|
514 |
+
"\n",
|
515 |
+
" # avg rating of each guessed word relative to all other words possible at that moment -- this should consistently be 100 for the algorithm, but will be different for user\n",
|
516 |
+
" if len(guess_entropies) > 1: # in case of guessing it correctly on the first try\n",
|
517 |
+
" sum_entropies = 0\n",
|
518 |
+
" for rating in guess_entropies:\n",
|
519 |
+
" sum_entropies += rating\n",
|
520 |
+
"\n",
|
521 |
+
" average_rating = float(round(sum_entropies / len(guess_entropies), 2))\n",
|
522 |
+
" stats_dict['avg_intermediate_guess_rating'] = average_rating\n",
|
523 |
+
" else:\n",
|
524 |
+
" stats_dict['avg_intermediate_guess_rating'] = float(100)\n",
|
525 |
+
"\n",
|
526 |
+
" expected_guesses = 3.85\n",
|
527 |
+
"\n",
|
528 |
+
" # guess_num = 3\n",
|
529 |
+
" # average_rating = 95\n",
|
530 |
+
" luck = round(1 - ((((guess_num / expected_guesses) * (stats_dict['avg_intermediate_guess_rating'] / 100)) / max_guesses) * 5), 2)\n",
|
531 |
+
" stats_dict['luck'] = luck\n",
|
532 |
+
"\n",
|
533 |
+
" if record == True:\n",
|
534 |
+
" if verbose == True:\n",
|
535 |
+
" with open(f\"solutions/{guessed_words[0]}_{target}_wizard_detailed.txt\", \"w\") as fout:\n",
|
536 |
+
" \n",
|
537 |
+
" fout.write(line + \"\\n\") # write each line of list of # printed text to .txt file\n",
|
538 |
+
" else:\n",
|
539 |
+
" with open(f\"solutions/{guessed_words[0]}_{target}_wizard_summary.txt\", \"w\") as fout:\n",
|
540 |
+
" \n",
|
541 |
+
" fout.write(line + \"\\n\") # write\n",
|
542 |
+
"\n",
|
543 |
+
" if guess_num <= 6:\n",
|
544 |
+
" stats_dict['valid_success'] = True\n",
|
545 |
+
" else:\n",
|
546 |
+
" stats_dict['valid_success'] = False\n",
|
547 |
+
"\n",
|
548 |
+
" stats_dict['num_guesses'] = float(guess_num)"
|
549 |
+
]
|
550 |
+
},
|
551 |
+
{
|
552 |
+
"cell_type": "code",
|
553 |
+
"execution_count": 37,
|
554 |
+
"metadata": {},
|
555 |
+
"outputs": [],
|
556 |
+
"source": [
|
557 |
+
"def wordle_wizard_cheat(guesses: list, word_list: list, max_guesses: int = None, \n",
|
558 |
+
" target: str = None,\n",
|
559 |
+
" random_guess: bool = False, random_target: bool = False, \n",
|
560 |
+
" verbose: bool = False, drama: float = None, \n",
|
561 |
+
" return_stats: bool = False, record: bool = False):\n",
|
562 |
+
" \"\"\"\n",
|
563 |
+
" Mimicking the popular web game, this function matches a current word to a target word automatically, in the most statistically optimal way possible.\n",
|
564 |
+
"\n",
|
565 |
+
" Parameters:\n",
|
566 |
+
" ------\n",
|
567 |
+
" `word_list`: list\n",
|
568 |
+
" list of valid words to be considered\n",
|
569 |
+
" `guess`: str\n",
|
570 |
+
" a string -- must be the same length as `target_word`\n",
|
571 |
+
" `target`: str\n",
|
572 |
+
" a string -- must be the same length as `opening_word`\n",
|
573 |
+
" `max_guesses`: int\n",
|
574 |
+
" the maximum number of attempts allowed to solve the Wordle\n",
|
575 |
+
" `random_guess`: bool\n",
|
576 |
+
" if True, randomly chooses a starting word from all words within `word_list`. If False, passed starting word must be used instead\n",
|
577 |
+
" `random_target`: bool\n",
|
578 |
+
" if True, randomly chooses a target word from all words within `word_list`. If False, passed target word must be used instead\n",
|
579 |
+
" `verbose`: bool\n",
|
580 |
+
" if True, # st.writes progress and explanation of how function solves the puzzle. If False, # st.writes only the guessed word at each guess.\n",
|
581 |
+
" `drama`: float or int\n",
|
582 |
+
" if int provided, each guess' output is delayed by that number of seconds, else each output is shown as quickly as possible. For ~dRaMaTiC eFfEcT~\n",
|
583 |
+
" `return_stats`: bool\n",
|
584 |
+
" if True, # st.writes nothing and returns a dictionary of various statistics about the function's performance trying to solve the puzzle\n",
|
585 |
+
" `record`: bool\n",
|
586 |
+
" if True, creates a .txt file with the same information # st.writeed according to the indicated verbosity\n",
|
587 |
+
"\n",
|
588 |
+
" Returns:\n",
|
589 |
+
" ------\n",
|
590 |
+
" `stats_dict`: dict\n",
|
591 |
+
" dictionary containing various statistics about the function's performance trying to solve the puzzle\n",
|
592 |
+
" \"\"\"\n",
|
593 |
+
"\n",
|
594 |
+
" # guess = guess.lower()\n",
|
595 |
+
" target = target.lower()\n",
|
596 |
+
"\n",
|
597 |
+
" sugg_words = []\n",
|
598 |
+
"\n",
|
599 |
+
" for i in range(0, 20):\n",
|
600 |
+
" ran_int = random.randint(0, len(word_list) - 1)\n",
|
601 |
+
" word = word_list[ran_int]\n",
|
602 |
+
" sugg_words.append(word)\n",
|
603 |
+
"\n",
|
604 |
+
" guess = guesses[0]\n",
|
605 |
+
"\n",
|
606 |
+
" stats_dict = {}\n",
|
607 |
+
" stats_dict['first_guess'] = guess\n",
|
608 |
+
" stats_dict['target_word'] = target\n",
|
609 |
+
" stats_dict['first_guess_vowels'] = float(count_vows_cons(guess, y_vow = True)['vows'])\n",
|
610 |
+
" stats_dict['first_guess_consonants'] = float(count_vows_cons(guess, y_vow = True)['cons'])\n",
|
611 |
+
" stats_dict['target_vowels'] = float(count_vows_cons(target, y_vow = True)['vows'])\n",
|
612 |
+
" stats_dict['target_consonants'] = float(count_vows_cons(target, y_vow = True)['cons'])\n",
|
613 |
+
" \n",
|
614 |
+
" # get rating of the first guess word and target word in the entire word_list\n",
|
615 |
+
" for tup in get_word_rating(word_list, word_list, normalized = True):\n",
|
616 |
+
" if tup[0] == guess:\n",
|
617 |
+
" stats_dict['first_guess_rating'] = tup[1]\n",
|
618 |
+
" if tup[0] == target:\n",
|
619 |
+
" stats_dict['target_rating'] = tup[1]\n",
|
620 |
+
"\n",
|
621 |
+
" guess_entropies = []\n",
|
622 |
+
" guess_entropies.append(stats_dict['first_guess_rating'])\n",
|
623 |
+
"\n",
|
624 |
+
" # luck_guess_1 = round(1 - ((1 / len(word_list)) * guess_entropies[0] / 100), 2) * 100\n",
|
625 |
+
"\n",
|
626 |
+
" english_alphabet = \"abcdefghijklmnopqrstuvwxyz\"\n",
|
627 |
+
"\n",
|
628 |
+
" # word_list_sorted_counts = get_letter_counts(english_alphabet, word_list, sort = \"descending\")\n",
|
629 |
+
" word_list_sorted_counts = get_letter_counts(word_list = word_list, letters = english_alphabet, sort = \"descending\", unique = True)\n",
|
630 |
+
"\n",
|
631 |
+
" wordlen = len(guesses[0])\n",
|
632 |
+
" letter_positions = set(i for i in range(0, wordlen))\n",
|
633 |
+
"\n",
|
634 |
+
" guess_set = set()\n",
|
635 |
+
" perfect_dict = {}\n",
|
636 |
+
" wrong_pos_dict = {}\n",
|
637 |
+
" wrong_pos_set = set()\n",
|
638 |
+
" dont_guess_again = set()\n",
|
639 |
+
"\n",
|
640 |
+
" guessed_words = [] # running set of guessed words\n",
|
641 |
+
" guess_num = 0 # baseline for variable\n",
|
642 |
+
" dont_guess_words = set()\n",
|
643 |
+
" incorrect_positions = []\n",
|
644 |
+
" reduction_per_guess = []\n",
|
645 |
+
"\n",
|
646 |
+
" if max_guesses == None: # if no value is passed, default is len(guess)\n",
|
647 |
+
" max_guesses = wordlen\n",
|
648 |
+
" else: # else it is the value passed\n",
|
649 |
+
" max_guesses = max_guesses\n",
|
650 |
+
"\n",
|
651 |
+
" perfect_letts_per_guess = []\n",
|
652 |
+
" wrong_pos_per_guess = []\n",
|
653 |
+
" wrong_letts_per_guess = []\n",
|
654 |
+
"\n",
|
655 |
+
" # while guess: # while there is any guess -- there are conditions to break it at the bottom\n",
|
656 |
+
"\n",
|
657 |
+
" for guess_num, guess in enumerate(guesses):\n",
|
658 |
+
"\n",
|
659 |
+
" guess_num += 1\n",
|
660 |
+
"\n",
|
661 |
+
" guessed_words.append(guess)\n",
|
662 |
+
"\n",
|
663 |
+
" if drama:\n",
|
664 |
+
" time.sleep(drama)\n",
|
665 |
+
"\n",
|
666 |
+
" # guess_num += 1 # each time the guess is processed\n",
|
667 |
+
" if return_stats == False:\n",
|
668 |
+
" if guess_num == 1:\n",
|
669 |
+
" st.write(\"-----------------------------\\n\")\n",
|
670 |
+
"\n",
|
671 |
+
" if guess == target:\n",
|
672 |
+
" stats_dict['target_guessed'] = True\n",
|
673 |
+
" if return_stats == False:\n",
|
674 |
+
" if guess_num == 1:\n",
|
675 |
+
" # st.write(f\"Congratulations! The Wordle has been solved in {guess_num} guess, that's amazingly lucky!\")\n",
|
676 |
+
" st.write(f\"The starting word and target word are the same. Try entering two different words to see how the puzzle can be solved.\")\n",
|
677 |
+
" # st.write(f\"The target word was {target}\")\n",
|
678 |
+
" \n",
|
679 |
+
" \n",
|
680 |
+
" perfect_letts_per_guess.append(5)\n",
|
681 |
+
" wrong_pos_per_guess.append(0)\n",
|
682 |
+
" wrong_letts_per_guess.append(0)\n",
|
683 |
+
" break\n",
|
684 |
+
" \n",
|
685 |
+
" if return_stats == False:\n",
|
686 |
+
" st.write(f\"**Guess {guess_num}: '{guess}'**\")\n",
|
687 |
+
"\n",
|
688 |
+
" guess_set = set()\n",
|
689 |
+
" wrong_pos_set = set()\n",
|
690 |
+
"\n",
|
691 |
+
" #### Step 2 -- ALL PERFECT\n",
|
692 |
+
" for i in letter_positions: # number of letters in each word (current word and target word)\n",
|
693 |
+
" guess_set.add(guess[i])\n",
|
694 |
+
"\n",
|
695 |
+
" if guess[i] not in perfect_dict:\n",
|
696 |
+
" perfect_dict[guess[i]] = set()\n",
|
697 |
+
" if guess[i] not in wrong_pos_dict:\n",
|
698 |
+
" wrong_pos_dict[guess[i]] = set()\n",
|
699 |
+
"\n",
|
700 |
+
" ### EVALUATE CURRENT GUESS\n",
|
701 |
+
" if guess[i] == target[i]: # letter == correct and position == correct\n",
|
702 |
+
" perfect_dict[guess[i]].add(i)\n",
|
703 |
+
"\n",
|
704 |
+
" if (guess[i] != target[i] and guess[i] in target): # letter == correct and position != correct\n",
|
705 |
+
" wrong_pos_dict[guess[i]].add(i)\n",
|
706 |
+
" wrong_pos_set.add(guess[i])\n",
|
707 |
+
"\n",
|
708 |
+
" if guess[i] not in target: # if letter is not relevant at all\n",
|
709 |
+
" dont_guess_again.add(guess[i])\n",
|
710 |
+
"\n",
|
711 |
+
" #### Step 3 -- ALL PERFECT\n",
|
712 |
+
" next_letters = set()\n",
|
713 |
+
" for letter, positions in perfect_dict.items():\n",
|
714 |
+
" if len(positions) > 0:\n",
|
715 |
+
" next_letters.add(letter)\n",
|
716 |
+
"\n",
|
717 |
+
" for letter, positions in wrong_pos_dict.items():\n",
|
718 |
+
" if len(positions) > 0:\n",
|
719 |
+
" next_letters.add(letter)\n",
|
720 |
+
"\n",
|
721 |
+
" #### List of tuples of correct letter positions in new valid words. Eg: [('e', 2), ('a', 3)]\n",
|
722 |
+
" perfect_letters = []\n",
|
723 |
+
" for letter, positions in perfect_dict.items():\n",
|
724 |
+
" for pos in positions:\n",
|
725 |
+
" if len(positions) > 0:\n",
|
726 |
+
" perfect_letters.append((letter, pos))\n",
|
727 |
+
"\n",
|
728 |
+
" #### all words that have correct letters in same spots\n",
|
729 |
+
" words_matching_correct_all = []\n",
|
730 |
+
" for word in word_list:\n",
|
731 |
+
" word_set = set()\n",
|
732 |
+
" for letter, pos in perfect_letters:\n",
|
733 |
+
" if pos < len(word):\n",
|
734 |
+
" if word[pos] == letter:\n",
|
735 |
+
" words_matching_correct_all.append(word)\n",
|
736 |
+
"\n",
|
737 |
+
" #### excluding words with letters in known incorrect positions\n",
|
738 |
+
" for letter, positions in wrong_pos_dict.items():\n",
|
739 |
+
" for pos in positions:\n",
|
740 |
+
" if len(positions) > 0:\n",
|
741 |
+
" if (letter, pos) not in incorrect_positions:\n",
|
742 |
+
" incorrect_positions.append((letter, pos))\n",
|
743 |
+
"\n",
|
744 |
+
" # sorting lists of tuples just to make them look nice in the # st.writeout\n",
|
745 |
+
" incorrect_positions = sorted(incorrect_positions, key = operator.itemgetter(1), reverse = False)\n",
|
746 |
+
" perfect_letters = sorted(perfect_letters, key = operator.itemgetter(1), reverse = False)\n",
|
747 |
+
"\n",
|
748 |
+
" #### all words that have correct letters in incorrect spots -- so they can be excluded efficiently\n",
|
749 |
+
" \n",
|
750 |
+
" # st.write(incorrect_positions)\n",
|
751 |
+
" \n",
|
752 |
+
" for word in word_list:\n",
|
753 |
+
" word_set = set()\n",
|
754 |
+
" for letter, pos in incorrect_positions:\n",
|
755 |
+
" if pos < len(word):\n",
|
756 |
+
" if word[pos] == letter:\n",
|
757 |
+
" dont_guess_words.add(word)\n",
|
758 |
+
" for word in word_list:\n",
|
759 |
+
" word_set = set()\n",
|
760 |
+
" for letter, pos in incorrect_positions:\n",
|
761 |
+
" if pos < len(word):\n",
|
762 |
+
" if word[pos] == letter:\n",
|
763 |
+
" dont_guess_words.add(word)\n",
|
764 |
+
"\n",
|
765 |
+
" for bad_letter in dont_guess_again:\n",
|
766 |
+
" for word in word_list:\n",
|
767 |
+
" if (bad_letter in word and word not in dont_guess_words):\n",
|
768 |
+
" dont_guess_words.add(word)\n",
|
769 |
+
"\n",
|
770 |
+
" if return_stats == False:\n",
|
771 |
+
" if verbose == True:\n",
|
772 |
+
" st.write(f\"Letters in correct positions:\\n\\t{perfect_letters}\\n\")\n",
|
773 |
+
" st.write(f\"Letters in incorrect positions:\\n\\t{incorrect_positions}\\n\")\n",
|
774 |
+
" # st.write (f\"Letters to guess again:\\n\\t{sorted(list(next_letters), reverse = False)}\\n\")\n",
|
775 |
+
" st.write(f\"Letters to not guess again:\\n\\t{sorted(list(dont_guess_again), reverse = False)}\\n\") # works\n",
|
776 |
+
"\n",
|
777 |
+
" # Returns True\n",
|
778 |
+
" # st.write(A.issubset(B)) # \"if everything in A is in B\", returns Bool\n",
|
779 |
+
"\n",
|
780 |
+
" perfect_letts_per_guess.append(len(perfect_letters))\n",
|
781 |
+
" wrong_pos_per_guess.append(len(incorrect_positions))\n",
|
782 |
+
" wrong_letts_per_guess.append(len(dont_guess_again))\n",
|
783 |
+
"\n",
|
784 |
+
" potential_next_guesses = set()\n",
|
785 |
+
" middle_set = set()\n",
|
786 |
+
"\n",
|
787 |
+
" if len(perfect_letters) == 0 and len(incorrect_positions) == 0: # if there are NEITHER perfect letters, NOR incorrect positions, ....\n",
|
788 |
+
" for word in word_list:\n",
|
789 |
+
" if word not in dont_guess_words:\n",
|
790 |
+
" if word not in guessed_words:\n",
|
791 |
+
" potential_next_guesses.add(word)\n",
|
792 |
+
" \n",
|
793 |
+
" # st.write(f\"GUESS {guess_num} : TEST 1-1\")\n",
|
794 |
+
"\n",
|
795 |
+
" if len(perfect_letters) == 0 and len(incorrect_positions) != 0: # if there are no perfect letters whatsoever, but there ARE incorrect positions ....\n",
|
796 |
+
" for word in word_list:\n",
|
797 |
+
" for incor_letter, incor_pos in incorrect_positions:\n",
|
798 |
+
" if incor_pos < len(word):\n",
|
799 |
+
" if word[incor_pos] != incor_letter:\n",
|
800 |
+
" if word not in dont_guess_words: # just in case\n",
|
801 |
+
" word_set = set()\n",
|
802 |
+
" for letter in word:\n",
|
803 |
+
" word_set.add(letter)\n",
|
804 |
+
" \n",
|
805 |
+
" if next_letters.issubset(word_set):\n",
|
806 |
+
" if word not in guessed_words:\n",
|
807 |
+
" if len(dont_guess_again) > 0:\n",
|
808 |
+
" for bad_letter in dont_guess_again:\n",
|
809 |
+
" if bad_letter not in word:\n",
|
810 |
+
" # potential_next_guesses.append(word)\n",
|
811 |
+
" potential_next_guesses.add(word)\n",
|
812 |
+
" else:\n",
|
813 |
+
" potential_next_guesses.add(word)\n",
|
814 |
+
" \n",
|
815 |
+
" # st.write(f\"GUESS {guess_num} : TEST 2-1\")\n",
|
816 |
+
"\n",
|
817 |
+
" else:\n",
|
818 |
+
" for word in word_list:\n",
|
819 |
+
" if word not in dont_guess_words: # just in case\n",
|
820 |
+
" word_set = set()\n",
|
821 |
+
" for letter in word:\n",
|
822 |
+
" word_set.add(letter)\n",
|
823 |
+
" if next_letters.issubset(word_set):\n",
|
824 |
+
" if word not in guessed_words:\n",
|
825 |
+
" # # st.write (\"TEST 3-2\")\n",
|
826 |
+
"\n",
|
827 |
+
" if len(dont_guess_again) > 0:\n",
|
828 |
+
" for bad_letter in dont_guess_again:\n",
|
829 |
+
" if bad_letter not in word:\n",
|
830 |
+
" middle_set.add(word)\n",
|
831 |
+
" else:\n",
|
832 |
+
" middle_set.add(word)\n",
|
833 |
+
" for word in middle_set:\n",
|
834 |
+
" dummy_list = []\n",
|
835 |
+
" for good_lett, good_pos in perfect_letters:\n",
|
836 |
+
" if word[good_pos] == good_lett:\n",
|
837 |
+
" dummy_list.append(1)\n",
|
838 |
+
" if len(dummy_list) == len(perfect_letters):\n",
|
839 |
+
" potential_next_guesses.add(word)\n",
|
840 |
+
" for word in middle_set:\n",
|
841 |
+
" dummy_list = []\n",
|
842 |
+
" for bad_lett, bad_pos in incorrect_positions:\n",
|
843 |
+
" if bad_pos < len(word):\n",
|
844 |
+
" if word[bad_pos] == bad_lett:\n",
|
845 |
+
" dummy_list.append(1)\n",
|
846 |
+
" if len(dummy_list) > 0:\n",
|
847 |
+
" potential_next_guesses.remove(word)\n",
|
848 |
+
" \n",
|
849 |
+
" # st.write(f\"GUESS {guess_num} : TEST 3-1\")\n",
|
850 |
+
"\n",
|
851 |
+
" if return_stats == False:\n",
|
852 |
+
" if verbose == True:\n",
|
853 |
+
" if len(potential_next_guesses) > 1:\n",
|
854 |
+
" # st.write(f\"At this point:\")\n",
|
855 |
+
" st.write(f\"\\t{len(word_list) - len(potential_next_guesses)}, {round((len(word_list) - len(potential_next_guesses)) / len(word_list) * 100, 2)}% of total words have been eliminated, and\")\n",
|
856 |
+
" st.write(f\"\\t{len(potential_next_guesses)}, {round(len(potential_next_guesses) / len(word_list) * 100, 2)}% of total words remain possible.\\n\")\n",
|
857 |
+
" \n",
|
858 |
+
" else:\n",
|
859 |
+
" # st.write(f\"At this point:\")\n",
|
860 |
+
" st.write(f\"\\t{len(word_list) - len(potential_next_guesses)}, {round((len(word_list) - len(potential_next_guesses)) / len(word_list) * 100, 2)}% of total words have been eliminated, and\")\n",
|
861 |
+
" st.write(f\"\\t{len(potential_next_guesses)}, {round(len(potential_next_guesses) / len(word_list) * 100, 2)}% of total words remain possible.\\n\")\n",
|
862 |
+
" \n",
|
863 |
+
" reduction_per_guess.append(len(potential_next_guesses))\n",
|
864 |
+
" \n",
|
865 |
+
" #### Guessing next word\n",
|
866 |
+
" if len(potential_next_guesses) == 1:\n",
|
867 |
+
"\n",
|
868 |
+
" if return_stats == False:\n",
|
869 |
+
" if verbose == True:\n",
|
870 |
+
" st.write(f\"All potential next guesses:\\n\\t{get_word_rating(words_to_rate = list(potential_next_guesses), word_list = word_list)}\\n\")\n",
|
871 |
+
" st.write(f\"Words guessed so far:\\n\\t{guessed_words}.\\n\")\n",
|
872 |
+
" \n",
|
873 |
+
" st.write(f\"The only remaining possible word is:\\n\\t'{list(potential_next_guesses)[0]}'\")\n",
|
874 |
+
" \n",
|
875 |
+
" # guess = list(potential_next_guesses)[0]\n",
|
876 |
+
" if guess_num < len(guesses):\n",
|
877 |
+
" guess = guesses[guess_num]\n",
|
878 |
+
" guess_entropies.append(get_word_rating([guess], word_list, normalized = False, ascending = False)[0][1])\n",
|
879 |
+
"\n",
|
880 |
+
" else:\n",
|
881 |
+
"\n",
|
882 |
+
" best_next_guesses = list(potential_next_guesses) \n",
|
883 |
+
" # # st.write (best_next_guesses)\n",
|
884 |
+
" word_ratings = get_word_rating(best_next_guesses, word_list, normalized = False, ascending = False) # \"internal\" ratings\n",
|
885 |
+
" \n",
|
886 |
+
" # Get max rating of all words\n",
|
887 |
+
" max_rating = -np.inf\n",
|
888 |
+
" for word, rating in word_ratings:\n",
|
889 |
+
" if rating > max_rating:\n",
|
890 |
+
" max_rating = rating\n",
|
891 |
+
"\n",
|
892 |
+
" # add best rated words (all equally best rating in next guess list) to set\n",
|
893 |
+
" best_of_the_best_1 = []\n",
|
894 |
+
" for word, rating in word_ratings:\n",
|
895 |
+
" if rating == max_rating:\n",
|
896 |
+
" best_of_the_best_1.append(word)\n",
|
897 |
+
"\n",
|
898 |
+
" # only using top ten most frequent prefixes suffixes to bias. After that it the impact is especially negligible\n",
|
899 |
+
" test_starts = get_gram_freq(word_list = word_list, letters_length = 1, position = \"start\", search = None)[:10]\n",
|
900 |
+
" test_ends = get_gram_freq(word_list = word_list, letters_length = 1, position = \"end\", search = None)[:10]\n",
|
901 |
+
"\n",
|
902 |
+
" # list of the best words that also have the most frequent starting and ending letters (suffixes and prefixes didn't have an impact)\n",
|
903 |
+
" best_of_the_best_2 = []\n",
|
904 |
+
" for start_gram, start_count in test_starts:\n",
|
905 |
+
" for end_gram, end_count in test_ends:\n",
|
906 |
+
" for word in best_of_the_best_1:\n",
|
907 |
+
" if word[:1] == start_gram and word[-1:] == end_gram:\n",
|
908 |
+
" best_of_the_best_2.append(word)\n",
|
909 |
+
"\n",
|
910 |
+
" # if len(best_of_the_best_2) > 0:\n",
|
911 |
+
" # guess = best_of_the_best_2[0]\n",
|
912 |
+
" # else:\n",
|
913 |
+
" # guess = best_of_the_best_1[0] # they're all equally the best of the best possible guesses so just pick the first\n",
|
914 |
+
"\n",
|
915 |
+
" if guess_num < len(guesses):\n",
|
916 |
+
" guess = guesses[guess_num]\n",
|
917 |
+
" \n",
|
918 |
+
" # guess_entropies.append(get_word_rating([guess], word_list, normalized = False, ascending = False)[0][1])\n",
|
919 |
+
"\n",
|
920 |
+
" if return_stats == False:\n",
|
921 |
+
" if verbose == True:\n",
|
922 |
+
" if len(word_ratings) <= 40:\n",
|
923 |
+
" st.write(f\"All potential next guesses:\\n\\t{word_ratings}\\n\")\n",
|
924 |
+
" st.write(f\"Words guessed so far:\\n\\t{guessed_words}.\\n\")\n",
|
925 |
+
" else:\n",
|
926 |
+
" st.write(f\"The top 40 potential next guesses are:\\n\\t{word_ratings[:40]}\\n\")\n",
|
927 |
+
" st.write(f\"Words guessed so far:\\n\\t{guessed_words}.\\n\")\n",
|
928 |
+
"\n",
|
929 |
+
" guess_entropies.append(get_word_rating([guess], word_list, normalized = False, ascending = False)[0][1])\n",
|
930 |
+
"\n",
|
931 |
+
" #### Guess has now been made -- what to do next\n",
|
932 |
+
" if guess_num == max_guesses: # if at max guesses allowed\n",
|
933 |
+
" guessed_words.append(guess)\n",
|
934 |
+
" stats_dict['target_guessed'] = False\n",
|
935 |
+
" if return_stats == False:\n",
|
936 |
+
" if verbose == True:\n",
|
937 |
+
" st.write(\"-----------------------------\\n\")\n",
|
938 |
+
" st.write(f\"\\nUnfortunately, the puzzle was not solved in {max_guesses} guesses. Better luck next time!\")\n",
|
939 |
+
" st.write(f\"The target word was '{target}'.\\n\")\n",
|
940 |
+
" st.write(\"-----------------------------\\n\")\n",
|
941 |
+
" else:\n",
|
942 |
+
" st.write(f\"\\nUnfortunately, the puzzle was not solved in {max_guesses} guesses. Better luck next time!\")\n",
|
943 |
+
" st.write(f\"The target word was '{target}'.\\n\")\n",
|
944 |
+
" break\n",
|
945 |
+
" else: # if not at max guesses yet allowed\n",
|
946 |
+
" # stats_dict['target_guessed'] = False\n",
|
947 |
+
" if return_stats == False:\n",
|
948 |
+
" if verbose == True:\n",
|
949 |
+
" if len(potential_next_guesses) > 1:\n",
|
950 |
+
" st.write(f\"Recommended next guess:\\n\\t'{word_ratings[0][0]}'\")\n",
|
951 |
+
" \n",
|
952 |
+
" # st.write(f\"Next guess:\\n\\t'{guess}'\")\n",
|
953 |
+
" st.write(\"\\n-----------------------------\\n\")\n",
|
954 |
+
"\n",
|
955 |
+
" if guess == target:\n",
|
956 |
+
" guess_num += 1\n",
|
957 |
+
" guessed_words.append(guess)\n",
|
958 |
+
" stats_dict['target_guessed'] = True\n",
|
959 |
+
"\n",
|
960 |
+
" if return_stats == False:\n",
|
961 |
+
" st.write(f\"**Guess {guess_num}: '{guess}'**\\n\")\n",
|
962 |
+
" st.write(f\"You solved the puzzle in {guess_num} guesses!\")\n",
|
963 |
+
"\n",
|
964 |
+
" if max_guesses - guess_num == 1:\n",
|
965 |
+
" st.write(f\"There was only {max_guesses - guess_num} guess remaining.\")\n",
|
966 |
+
" else:\n",
|
967 |
+
" st.write(f\"There were still {max_guesses - guess_num} guesses remaining.\")\n",
|
968 |
+
"\n",
|
969 |
+
" if return_stats == False: \n",
|
970 |
+
" # stats_dict['target_guessed'] = True \n",
|
971 |
+
" st.write(f\"\\nThe target word was **'{target}'**.\")\n",
|
972 |
+
" st.write(\"\\n-----------------------------\")\n",
|
973 |
+
" break\n",
|
974 |
+
"\n",
|
975 |
+
" #### STATS STUFF \n",
|
976 |
+
" mid_guesses_vows = 0\n",
|
977 |
+
" mid_guesses_cons = 0\n",
|
978 |
+
" avg_perf_letters = 0\n",
|
979 |
+
" avg_wrong_pos_letters = 0\n",
|
980 |
+
" avg_wrong_letters = 0\n",
|
981 |
+
"\n",
|
982 |
+
" for i, word in enumerate(guessed_words):\n",
|
983 |
+
" mid_guesses_vows += count_vows_cons(word, y_vow = True)['vows']\n",
|
984 |
+
" mid_guesses_cons += count_vows_cons(word, y_vow = True)['cons']\n",
|
985 |
+
" \n",
|
986 |
+
" for i in range(0, len(guessed_words) - 1):\n",
|
987 |
+
" avg_perf_letters += perfect_letts_per_guess[i]\n",
|
988 |
+
" avg_wrong_pos_letters += wrong_pos_per_guess[i]\n",
|
989 |
+
" avg_wrong_letters += wrong_letts_per_guess[i]\n",
|
990 |
+
"\n",
|
991 |
+
" stats_dict['mid_guesses_avg_vows'] = float(round(mid_guesses_vows / len(guessed_words), 2))\n",
|
992 |
+
" stats_dict['mid_guesses_avg_cons'] = float(round(mid_guesses_cons / len(guessed_words), 2))\n",
|
993 |
+
"\n",
|
994 |
+
" stats_dict['avg_perf_letters'] = float(round(np.mean(avg_perf_letters), 2))\n",
|
995 |
+
" stats_dict['avg_wrong_pos_letters'] = float(round(np.mean(avg_wrong_pos_letters), 2))\n",
|
996 |
+
" stats_dict['avg_wrong_letters'] = float(round(np.mean(avg_wrong_letters), 2))\n",
|
997 |
+
" \n",
|
998 |
+
" # average number of words remaining after each guess -- the higher this is, the luckier the person got (the lower, the more guesses it took)\n",
|
999 |
+
" stats_dict['avg_remaining'] = float(round(np.mean(reduction_per_guess), 2))\n",
|
1000 |
+
"\n",
|
1001 |
+
" # avg rating of each guessed word relative to all other words possible at that moment -- this should consistently be 100 for the algorithm, but will be different for user\n",
|
1002 |
+
" if len(guess_entropies) > 1: # in case of guessing it correctly on the first try\n",
|
1003 |
+
" sum_entropies = 0\n",
|
1004 |
+
" for rating in guess_entropies:\n",
|
1005 |
+
" sum_entropies += rating\n",
|
1006 |
+
"\n",
|
1007 |
+
" average_rating = float(round(sum_entropies / len(guess_entropies), 2))\n",
|
1008 |
+
" stats_dict['avg_intermediate_guess_rating'] = average_rating\n",
|
1009 |
+
" else:\n",
|
1010 |
+
" stats_dict['avg_intermediate_guess_rating'] = float(100)\n",
|
1011 |
+
"\n",
|
1012 |
+
" expected_guesses = 3.85\n",
|
1013 |
+
"\n",
|
1014 |
+
" # guess_num = 3\n",
|
1015 |
+
" # average_rating = 95\n",
|
1016 |
+
" luck = round(1 - ((((guess_num / expected_guesses) * (stats_dict['avg_intermediate_guess_rating'] / 100)) / max_guesses) * 5), 2)\n",
|
1017 |
+
" stats_dict['luck'] = luck\n",
|
1018 |
+
"\n",
|
1019 |
+
" if record == True:\n",
|
1020 |
+
" if verbose == True:\n",
|
1021 |
+
" with open(f\"solutions/{guessed_words[0]}_{target}_wizard_detailed.txt\", \"w\") as fout:\n",
|
1022 |
+
" \n",
|
1023 |
+
" fout.write(line + \"\\n\") # write each line of list of # st.writeed text to .txt file\n",
|
1024 |
+
" else:\n",
|
1025 |
+
" with open(f\"solutions/{guessed_words[0]}_{target}_wizard_summary.txt\", \"w\") as fout:\n",
|
1026 |
+
" \n",
|
1027 |
+
" fout.write(line + \"\\n\") # write\n",
|
1028 |
+
"\n",
|
1029 |
+
" if guess_num <= 6:\n",
|
1030 |
+
" stats_dict['valid_success'] = True\n",
|
1031 |
+
" else:\n",
|
1032 |
+
" stats_dict['valid_success'] = False\n",
|
1033 |
+
"\n",
|
1034 |
+
" stats_dict['num_guesses'] = float(guess_num)"
|
1035 |
+
]
|
1036 |
+
},
|
1037 |
+
{
|
1038 |
+
"cell_type": "code",
|
1039 |
+
"execution_count": 14,
|
1040 |
+
"metadata": {},
|
1041 |
+
"outputs": [
|
1042 |
+
{
|
1043 |
+
"name": "stdout",
|
1044 |
+
"output_type": "stream",
|
1045 |
+
"text": [
|
1046 |
+
"-----------------------------\n",
|
1047 |
+
"\n",
|
1048 |
+
"**Guess 1: 'tried'**\n",
|
1049 |
+
"Letters in correct positions:\n",
|
1050 |
+
"\t[('t', 0)]\n",
|
1051 |
+
"\n",
|
1052 |
+
"Letters in incorrect positions:\n",
|
1053 |
+
"\t[('e', 3)]\n",
|
1054 |
+
"\n",
|
1055 |
+
"Letters to not guess again:\n",
|
1056 |
+
"\t['d', 'i', 'r']\n",
|
1057 |
+
"\n",
|
1058 |
+
"\t2293, 99.22% of total words have been eliminated, and\n",
|
1059 |
+
"\t18, 0.78% of total words remain possible.\n",
|
1060 |
+
"\n",
|
1061 |
+
"All potential next guesses:\n",
|
1062 |
+
"\t[('table', 32.95), ('teach', 32.13), ('those', 31.55), ('taste', 30.22), ('tease', 30.22), ('tempo', 28.27), ('tweak', 28.14), ('theta', 27.97), ('tense', 26.89), ('tulle', 26.28), ('thyme', 26.19), ('testy', 25.66), ('these', 25.29), ('tenth', 24.64), ('theme', 22.31), ('tests', 21.78), ('theft', 21.46), ('teeth', 19.54)]\n",
|
1063 |
+
"\n",
|
1064 |
+
"Words guessed so far:\n",
|
1065 |
+
"\t['tried'].\n",
|
1066 |
+
"\n",
|
1067 |
+
"Recommended next guess:\n",
|
1068 |
+
"\t'table'\n",
|
1069 |
+
"\n",
|
1070 |
+
"-----------------------------\n",
|
1071 |
+
"\n",
|
1072 |
+
"**Guess 2: 'score'**\n",
|
1073 |
+
"Letters in correct positions:\n",
|
1074 |
+
"\t[('t', 0)]\n",
|
1075 |
+
"\n",
|
1076 |
+
"Letters in incorrect positions:\n",
|
1077 |
+
"\t[('s', 0), ('e', 3), ('e', 4)]\n",
|
1078 |
+
"\n",
|
1079 |
+
"Letters to not guess again:\n",
|
1080 |
+
"\t['c', 'd', 'i', 'o', 'r']\n",
|
1081 |
+
"\n",
|
1082 |
+
"\t2309, 99.91% of total words have been eliminated, and\n",
|
1083 |
+
"\t2, 0.09% of total words remain possible.\n",
|
1084 |
+
"\n",
|
1085 |
+
"All potential next guesses:\n",
|
1086 |
+
"\t[('testy', 25.66), ('tests', 21.78)]\n",
|
1087 |
+
"\n",
|
1088 |
+
"Words guessed so far:\n",
|
1089 |
+
"\t['tried', 'score'].\n",
|
1090 |
+
"\n",
|
1091 |
+
"Recommended next guess:\n",
|
1092 |
+
"\t'testy'\n",
|
1093 |
+
"\n",
|
1094 |
+
"-----------------------------\n",
|
1095 |
+
"\n",
|
1096 |
+
"**Guess 3: 'messy'**\n",
|
1097 |
+
"Letters in correct positions:\n",
|
1098 |
+
"\t[('t', 0), ('e', 1), ('s', 2)]\n",
|
1099 |
+
"\n",
|
1100 |
+
"Letters in incorrect positions:\n",
|
1101 |
+
"\t[('s', 0), ('e', 3), ('s', 3), ('e', 4)]\n",
|
1102 |
+
"\n",
|
1103 |
+
"Letters to not guess again:\n",
|
1104 |
+
"\t['c', 'd', 'i', 'm', 'o', 'r', 'y']\n",
|
1105 |
+
"\n",
|
1106 |
+
"\t2310, 99.96% of total words have been eliminated, and\n",
|
1107 |
+
"\t1, 0.04% of total words remain possible.\n",
|
1108 |
+
"\n",
|
1109 |
+
"All potential next guesses:\n",
|
1110 |
+
"\t[('tests', 100.0)]\n",
|
1111 |
+
"\n",
|
1112 |
+
"Words guessed so far:\n",
|
1113 |
+
"\t['tried', 'score', 'messy'].\n",
|
1114 |
+
"\n",
|
1115 |
+
"The only remaining possible word is:\n",
|
1116 |
+
"\t'tests'\n",
|
1117 |
+
"\n",
|
1118 |
+
"-----------------------------\n",
|
1119 |
+
"\n"
|
1120 |
+
]
|
1121 |
+
}
|
1122 |
+
],
|
1123 |
+
"source": [
|
1124 |
+
"guesses = [\n",
|
1125 |
+
" \"tried\",\n",
|
1126 |
+
" # \"tried\",\n",
|
1127 |
+
" \"score\",\n",
|
1128 |
+
" \"messy\",\n",
|
1129 |
+
" # \"traps\",\n",
|
1130 |
+
" # \"bummy\",\n",
|
1131 |
+
" # \"chore\"\n",
|
1132 |
+
"]\n",
|
1133 |
+
"\n",
|
1134 |
+
"wordle_wizard_cheat_local(guesses = guesses, word_list = official_words, max_guesses = 6, \n",
|
1135 |
+
" target = \"tests\",\n",
|
1136 |
+
" random_guess = False, random_target = False, \n",
|
1137 |
+
" verbose = True, drama = 0, return_stats = False, record = False)\n",
|
1138 |
+
"\n",
|
1139 |
+
"# wordle_wizard_cheat(guesses = guesses, word_list = official_words, max_guesses = 6, \n",
|
1140 |
+
"# target = \"force\",\n",
|
1141 |
+
"# random_guess = False, random_target = False, \n",
|
1142 |
+
"# verbose = True, drama = 0, return_stats = False, record = False)"
|
1143 |
+
]
|
1144 |
+
},
|
1145 |
+
{
|
1146 |
+
"cell_type": "markdown",
|
1147 |
+
"metadata": {},
|
1148 |
+
"source": [
|
1149 |
+
"# Testing Scraping + Assistant"
|
1150 |
+
]
|
1151 |
+
},
|
1152 |
+
{
|
1153 |
+
"cell_type": "code",
|
1154 |
+
"execution_count": 17,
|
1155 |
+
"metadata": {},
|
1156 |
+
"outputs": [
|
1157 |
+
{
|
1158 |
+
"name": "stderr",
|
1159 |
+
"output_type": "stream",
|
1160 |
+
"text": [
|
1161 |
+
"2023-07-17 11:37:27.782 INFO selenium.webdriver.common.selenium_manager: Executing: /opt/miniconda3/lib/python3.10/site-packages/selenium/webdriver/common/macos/selenium-manager --browser chrome --output json\n"
|
1162 |
+
]
|
1163 |
+
},
|
1164 |
+
{
|
1165 |
+
"name": "stdout",
|
1166 |
+
"output_type": "stream",
|
1167 |
+
"text": [
|
1168 |
+
"droop\n"
|
1169 |
+
]
|
1170 |
+
}
|
1171 |
+
],
|
1172 |
+
"source": [
|
1173 |
+
"# import requests\n",
|
1174 |
+
"from bs4 import BeautifulSoup\n",
|
1175 |
+
"\n",
|
1176 |
+
"# !pip install selenium\n",
|
1177 |
+
"from selenium import webdriver\n",
|
1178 |
+
"from selenium.webdriver.common.keys import Keys\n",
|
1179 |
+
"from selenium.webdriver.chrome.options import Options\n",
|
1180 |
+
"\n",
|
1181 |
+
"# Configure ChromeOptions\n",
|
1182 |
+
"chrome_options = Options()\n",
|
1183 |
+
"chrome_options.add_argument(\"--headless\") # Run Chrome in headless mode\n",
|
1184 |
+
"\n",
|
1185 |
+
"# driver = webdriver.Chrome()\n",
|
1186 |
+
"driver = webdriver.Chrome(options = chrome_options)\n",
|
1187 |
+
"\n",
|
1188 |
+
"url = \"https://screenrant.com/wordle-answers-updated-word-puzzle-guide/\"\n",
|
1189 |
+
"\n",
|
1190 |
+
"# Navigate to the URL\n",
|
1191 |
+
"driver.get(url)\n",
|
1192 |
+
"\n",
|
1193 |
+
"# Get the page source\n",
|
1194 |
+
"html_content = driver.page_source\n",
|
1195 |
+
" \n",
|
1196 |
+
"soup = BeautifulSoup(html_content, \"html.parser\")\n",
|
1197 |
+
"for item in soup.find_all('a'):\n",
|
1198 |
+
" \n",
|
1199 |
+
" item_link = item['href']\n",
|
1200 |
+
"\n",
|
1201 |
+
" link_prefix = \"https://screenrant.com/todays-wordle-answer-hints\"\n",
|
1202 |
+
"\n",
|
1203 |
+
" if item_link:\n",
|
1204 |
+
" \n",
|
1205 |
+
" if link_prefix in item_link:\n",
|
1206 |
+
" good_item = item\n",
|
1207 |
+
" break\n",
|
1208 |
+
"\n",
|
1209 |
+
"good_text = good_item.text\n",
|
1210 |
+
"target_word = good_text.split(\" - \")[-1].lower() # something like \"topaz\"\n",
|
1211 |
+
"print(target_word)"
|
1212 |
+
]
|
1213 |
+
},
|
1214 |
+
{
|
1215 |
+
"cell_type": "code",
|
1216 |
+
"execution_count": 18,
|
1217 |
+
"metadata": {},
|
1218 |
+
"outputs": [
|
1219 |
+
{
|
1220 |
+
"name": "stdout",
|
1221 |
+
"output_type": "stream",
|
1222 |
+
"text": [
|
1223 |
+
"-----------------------------\n",
|
1224 |
+
"\n",
|
1225 |
+
"**Guess 1: 'arose'**\n",
|
1226 |
+
"Letters in correct positions:\n",
|
1227 |
+
"\t[('r', 1), ('o', 2)]\n",
|
1228 |
+
"\n",
|
1229 |
+
"Letters in incorrect positions:\n",
|
1230 |
+
"\t[]\n",
|
1231 |
+
"\n",
|
1232 |
+
"Letters to not guess again:\n",
|
1233 |
+
"\t['a', 'e', 's']\n",
|
1234 |
+
"\n",
|
1235 |
+
"\t2273, 98.36% of total words have been eliminated, and\n",
|
1236 |
+
"\t38, 1.64% of total words remain possible.\n",
|
1237 |
+
"\n",
|
1238 |
+
"All potential next guesses:\n",
|
1239 |
+
"\t[('droit', 29.71), ('irony', 29.02), ('broil', 28.52), ('groin', 27.93), ('grout', 27.28), ('front', 27.27), ('crony', 27.16), ('troll', 26.25), ('broth', 26.23), ('froth', 25.67), ('croup', 25.64), ('prong', 25.13), ('crown', 25.08), ('prowl', 25.04), ('proud', 24.93), ('growl', 24.61), ('trout', 24.49), ('frond', 24.49), ('drown', 24.37), ('group', 24.27), ('wrong', 23.71), ('grown', 23.71), ('droll', 23.48), ('drool', 23.48), ('troop', 23.46), ('crowd', 23.42), ('brown', 23.4), ('frown', 22.84), ('frock', 21.98), ('proxy', 21.46), ('droop', 20.69), ('crook', 20.06), ('crock', 20.06), ('brood', 19.95), ('groom', 19.59), ('broom', 19.28), ('proof', 19.16), ('brook', 18.39)]\n",
|
1240 |
+
"\n",
|
1241 |
+
"Words guessed so far:\n",
|
1242 |
+
"\t['arose'].\n",
|
1243 |
+
"\n",
|
1244 |
+
"Recommended next guess:\n",
|
1245 |
+
"\t'droit'\n",
|
1246 |
+
"\n",
|
1247 |
+
"-----------------------------\n",
|
1248 |
+
"\n",
|
1249 |
+
"**Guess 2: 'droit'**\n",
|
1250 |
+
"Letters in correct positions:\n",
|
1251 |
+
"\t[('d', 0), ('r', 1), ('o', 2)]\n",
|
1252 |
+
"\n",
|
1253 |
+
"Letters in incorrect positions:\n",
|
1254 |
+
"\t[]\n",
|
1255 |
+
"\n",
|
1256 |
+
"Letters to not guess again:\n",
|
1257 |
+
"\t['a', 'e', 'i', 's', 't']\n",
|
1258 |
+
"\n",
|
1259 |
+
"\t2307, 99.83% of total words have been eliminated, and\n",
|
1260 |
+
"\t4, 0.17% of total words remain possible.\n",
|
1261 |
+
"\n",
|
1262 |
+
"All potential next guesses:\n",
|
1263 |
+
"\t[('drown', 24.37), ('drool', 23.48), ('droll', 23.48), ('droop', 20.69)]\n",
|
1264 |
+
"\n",
|
1265 |
+
"Words guessed so far:\n",
|
1266 |
+
"\t['arose', 'droit'].\n",
|
1267 |
+
"\n",
|
1268 |
+
"Recommended next guess:\n",
|
1269 |
+
"\t'drown'\n",
|
1270 |
+
"\n",
|
1271 |
+
"-----------------------------\n",
|
1272 |
+
"\n",
|
1273 |
+
"**Guess 3: 'drown'**\n",
|
1274 |
+
"Letters in correct positions:\n",
|
1275 |
+
"\t[('d', 0), ('r', 1), ('o', 2)]\n",
|
1276 |
+
"\n",
|
1277 |
+
"Letters in incorrect positions:\n",
|
1278 |
+
"\t[]\n",
|
1279 |
+
"\n",
|
1280 |
+
"Letters to not guess again:\n",
|
1281 |
+
"\t['a', 'e', 'i', 'n', 's', 't', 'w']\n",
|
1282 |
+
"\n",
|
1283 |
+
"\t2308, 99.87% of total words have been eliminated, and\n",
|
1284 |
+
"\t3, 0.13% of total words remain possible.\n",
|
1285 |
+
"\n",
|
1286 |
+
"All potential next guesses:\n",
|
1287 |
+
"\t[('drool', 23.48), ('droll', 23.48), ('droop', 20.69)]\n",
|
1288 |
+
"\n",
|
1289 |
+
"Words guessed so far:\n",
|
1290 |
+
"\t['arose', 'droit', 'drown'].\n",
|
1291 |
+
"\n",
|
1292 |
+
"Recommended next guess:\n",
|
1293 |
+
"\t'drool'\n",
|
1294 |
+
"\n",
|
1295 |
+
"-----------------------------\n",
|
1296 |
+
"\n",
|
1297 |
+
"**Guess 4: 'drool'**\n",
|
1298 |
+
"Letters in correct positions:\n",
|
1299 |
+
"\t[('d', 0), ('r', 1), ('o', 2), ('o', 3)]\n",
|
1300 |
+
"\n",
|
1301 |
+
"Letters in incorrect positions:\n",
|
1302 |
+
"\t[]\n",
|
1303 |
+
"\n",
|
1304 |
+
"Letters to not guess again:\n",
|
1305 |
+
"\t['a', 'e', 'i', 'l', 'n', 's', 't', 'w']\n",
|
1306 |
+
"\n",
|
1307 |
+
"\t2310, 99.96% of total words have been eliminated, and\n",
|
1308 |
+
"\t1, 0.04% of total words remain possible.\n",
|
1309 |
+
"\n",
|
1310 |
+
"All potential next guesses:\n",
|
1311 |
+
"\t[('droop', 100.0)]\n",
|
1312 |
+
"\n",
|
1313 |
+
"Words guessed so far:\n",
|
1314 |
+
"\t['arose', 'droit', 'drown', 'drool'].\n",
|
1315 |
+
"\n",
|
1316 |
+
"The only remaining possible word is:\n",
|
1317 |
+
"\t'droop'\n",
|
1318 |
+
"\n",
|
1319 |
+
"-----------------------------\n",
|
1320 |
+
"\n"
|
1321 |
+
]
|
1322 |
+
}
|
1323 |
+
],
|
1324 |
+
"source": [
|
1325 |
+
"guesses = [\n",
|
1326 |
+
" 'arose',\n",
|
1327 |
+
" 'droit',\n",
|
1328 |
+
" 'drown',\n",
|
1329 |
+
" 'drool'\n",
|
1330 |
+
"]\n",
|
1331 |
+
"\n",
|
1332 |
+
"wordle_wizard_cheat_local(guesses = guesses, word_list = official_words, max_guesses = 6, \n",
|
1333 |
+
" target = target_word,\n",
|
1334 |
+
" random_guess = False, random_target = False, \n",
|
1335 |
+
" verbose = True, drama = 0, return_stats = False, record = False)"
|
1336 |
+
]
|
1337 |
+
},
|
1338 |
+
{
|
1339 |
+
"cell_type": "code",
|
1340 |
+
"execution_count": 52,
|
1341 |
+
"metadata": {},
|
1342 |
+
"outputs": [],
|
1343 |
+
"source": [
|
1344 |
+
"# import streamlit as st\n",
|
1345 |
+
"# # import random\n",
|
1346 |
+
"# import plotly.express as px\n",
|
1347 |
+
"# from bs4 import BeautifulSoup\n",
|
1348 |
+
"# from selenium import webdriver\n",
|
1349 |
+
"# from selenium.webdriver.common.keys import Keys\n",
|
1350 |
+
"# from selenium.webdriver.chrome.options import Options\n",
|
1351 |
+
"# import bs4\n",
|
1352 |
+
"\n",
|
1353 |
+
"# # Since we can't import from streamlit_extras.stateful_button, wordle_assistant_functions, and plots directly, \n",
|
1354 |
+
"# # you should replace the placeholders with the correct modules.\n",
|
1355 |
+
"# # import streamlit_extras\n",
|
1356 |
+
"# # import wordle_assistant_functions\n",
|
1357 |
+
"# # import plots\n",
|
1358 |
+
"\n",
|
1359 |
+
"# print(\"streamlit:\", st.__version__)\n",
|
1360 |
+
"# # print(\"streamlit_extras:\", streamlit_extras.__version__)\n",
|
1361 |
+
"# print(\"random: This is a built-in module, so it doesn't have a version.\")\n",
|
1362 |
+
"# # print(\"wordle_assistant_functions:\", wordle_assistant_functions.__version__)\n",
|
1363 |
+
"# # print(\"plotly:\", plotly.version)\n",
|
1364 |
+
"# # print(\"plots:\", plots.__version__)\n",
|
1365 |
+
"# print(\"BeautifulSoup:\", bs4.__version__)\n",
|
1366 |
+
"# print(\"selenium:\", webdriver.__version__)"
|
1367 |
+
]
|
1368 |
+
}
|
1369 |
+
],
|
1370 |
+
"metadata": {
|
1371 |
+
"kernelspec": {
|
1372 |
+
"display_name": "base",
|
1373 |
+
"language": "python",
|
1374 |
+
"name": "python3"
|
1375 |
+
},
|
1376 |
+
"language_info": {
|
1377 |
+
"codemirror_mode": {
|
1378 |
+
"name": "ipython",
|
1379 |
+
"version": 3
|
1380 |
+
},
|
1381 |
+
"file_extension": ".py",
|
1382 |
+
"mimetype": "text/x-python",
|
1383 |
+
"name": "python",
|
1384 |
+
"nbconvert_exporter": "python",
|
1385 |
+
"pygments_lexer": "ipython3",
|
1386 |
+
"version": "3.10.2"
|
1387 |
+
},
|
1388 |
+
"orig_nbformat": 4
|
1389 |
+
},
|
1390 |
+
"nbformat": 4,
|
1391 |
+
"nbformat_minor": 2
|
1392 |
+
}
|
notebooks/wordle_assistant_functions.py
ADDED
@@ -0,0 +1,1350 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import numpy as np # for stats
|
2 |
+
import random # for randomly generating target and start words
|
3 |
+
import operator # for sorting letter frequency distribution
|
4 |
+
import time # for #dramaticeffect
|
5 |
+
import pandas as pd
|
6 |
+
import streamlit as st
|
7 |
+
|
8 |
+
english_alphabet = "abcdefghijklmnopqrstuvwxyz"
|
9 |
+
|
10 |
+
def get_letter_counts(word_list: list, letters: str = english_alphabet, sort: str = "descending", unique: bool = True):
|
11 |
+
"""
|
12 |
+
Given a passed str of letters and a list of words, produces a frequency distribution of all letters
|
13 |
+
|
14 |
+
Parameters:
|
15 |
+
------
|
16 |
+
`word_list`: list
|
17 |
+
list of words (str) from which word frequencies will be counted
|
18 |
+
`letters`: str
|
19 |
+
a string of letters to be counted. String must only be desired letters, with no spaces. Default is local variable containing all letters of the English alphabet
|
20 |
+
`sort`: str
|
21 |
+
if either "descending" or "ascending" are passed, returned list of tuples will be sorted accordingly, else returned dictionary will be unsorted
|
22 |
+
`unique`: bool
|
23 |
+
if True, only unique letters in a word are counted. That means that words with more unique letters are rated more highly than any words with duplicate letters
|
24 |
+
|
25 |
+
Returns:
|
26 |
+
------
|
27 |
+
`letters_counts_dict`: dict
|
28 |
+
dictionary of {letter : count} pairs for each letter in passed `letters` sequence
|
29 |
+
`sorted_counts_dicts`: list of tuples
|
30 |
+
list of tuples. Format is ("letter", frequency). Ordered according to `sort` values
|
31 |
+
"""
|
32 |
+
|
33 |
+
words_counts_dict = {}
|
34 |
+
|
35 |
+
if unique == False:
|
36 |
+
for word in word_list: # real dataset
|
37 |
+
word_dict = {}
|
38 |
+
|
39 |
+
for letter in word:
|
40 |
+
if letter in word_dict:
|
41 |
+
word_dict[letter] += 1
|
42 |
+
else:
|
43 |
+
word_dict[letter] = 1
|
44 |
+
words_counts_dict[word] = word_dict
|
45 |
+
else: # if unique == True
|
46 |
+
|
47 |
+
for word in word_list: # real dataset
|
48 |
+
word_dict = {}
|
49 |
+
|
50 |
+
word_letters = set(letter for letter in word)
|
51 |
+
|
52 |
+
for letter in word_letters:
|
53 |
+
if letter in word_dict:
|
54 |
+
word_dict[letter] += 1
|
55 |
+
else:
|
56 |
+
word_dict[letter] = 1
|
57 |
+
words_counts_dict[word] = word_dict
|
58 |
+
|
59 |
+
letters_counts_dict = {}
|
60 |
+
|
61 |
+
for letter in letters:
|
62 |
+
letters_counts_dict[letter] = 0
|
63 |
+
|
64 |
+
for word, count_dict in words_counts_dict.items():
|
65 |
+
# # print (word, count_dict)
|
66 |
+
for letter, count in count_dict.items():
|
67 |
+
letters_counts_dict[letter] += count
|
68 |
+
|
69 |
+
if sort == "ascending":
|
70 |
+
sorted_counts_dict = (sorted(letters_counts_dict.items(), key = operator.itemgetter(1), reverse = False))
|
71 |
+
return sorted_counts_dicts
|
72 |
+
|
73 |
+
if sort == "descending":
|
74 |
+
sorted_counts_dict = sorted(letters_counts_dict.items(), key = operator.itemgetter(1), reverse = True)
|
75 |
+
return sorted_counts_dict
|
76 |
+
else:
|
77 |
+
return letters_counts_dict
|
78 |
+
|
79 |
+
### Best first guesses for a given Wordle list
|
80 |
+
|
81 |
+
def best_guess_words(word_list: list, show_letters: bool = False):
|
82 |
+
"""
|
83 |
+
Given a passed list of English words of a consistent length, calculates the most statistically optimal first guess words, alongside a rating for each word.
|
84 |
+
|
85 |
+
Rating = sum(frequency of each unique letter in that word) / sum (all unique letter frequencies in word_list) * 100, rounded to 2 decimals.
|
86 |
+
|
87 |
+
------
|
88 |
+
Parameters:
|
89 |
+
------
|
90 |
+
`word_list`: list
|
91 |
+
list of words (str) of consistent length
|
92 |
+
`show_letters`: bool
|
93 |
+
if True, also # prints set of most optimal letters to guess
|
94 |
+
|
95 |
+
------
|
96 |
+
Returns:
|
97 |
+
------
|
98 |
+
`word_ratings`: list
|
99 |
+
list of tuples. Format is [(word, rating)], where rating is calculated according to above formula
|
100 |
+
`sorted_counts`: list of tuples
|
101 |
+
list of tuples. Format is ("letter", frequency). Sorted according to `sort` value; ["descending" or "ascending"] if passed
|
102 |
+
"""
|
103 |
+
|
104 |
+
english_alphabet = "abcdefghijklmnopqrstuvwxyz"
|
105 |
+
|
106 |
+
sorted_counts = get_letter_counts(english_alphabet, word_list, sort = "descending")
|
107 |
+
|
108 |
+
max_len_possible = len(word_list[0])
|
109 |
+
|
110 |
+
### Get words with the highest letter diversity
|
111 |
+
while max_len_possible:
|
112 |
+
|
113 |
+
best_letters = set()
|
114 |
+
best_words = []
|
115 |
+
|
116 |
+
for letter, freq in sorted_counts:
|
117 |
+
best_letters.add(letter)
|
118 |
+
if len(best_letters) == max_len_possible:
|
119 |
+
break
|
120 |
+
|
121 |
+
### Get all words that have one of each of the 5 top most frequent letters
|
122 |
+
for word in word_list:
|
123 |
+
word_set = set()
|
124 |
+
|
125 |
+
for letter in word:
|
126 |
+
word_set.add(letter)
|
127 |
+
|
128 |
+
if best_letters.issubset(word_set):
|
129 |
+
best_words.append(word)
|
130 |
+
|
131 |
+
if len(best_words) > 0:
|
132 |
+
break
|
133 |
+
else:
|
134 |
+
max_len_possible -= 1 # only try the top 4 letters, then 3, then 2, ...
|
135 |
+
|
136 |
+
if max_len_possible == 0:
|
137 |
+
break
|
138 |
+
|
139 |
+
all_letters_count = 0
|
140 |
+
for letter, freq in sorted_counts:
|
141 |
+
all_letters_count += freq
|
142 |
+
|
143 |
+
word_ratings = []
|
144 |
+
for word in best_words:
|
145 |
+
ratings_dict = {}
|
146 |
+
for letter in word:
|
147 |
+
for freq_letter, freq in sorted_counts:
|
148 |
+
if letter == freq_letter:
|
149 |
+
ratings_dict[letter] = freq
|
150 |
+
|
151 |
+
total_rating = 0
|
152 |
+
for letter, rating in ratings_dict.items():
|
153 |
+
total_rating += rating
|
154 |
+
|
155 |
+
word_ratings.append((word, round(total_rating / all_letters_count * 100, 2)))
|
156 |
+
|
157 |
+
word_ratings = sorted(word_ratings, key = operator.itemgetter(1), reverse = True)
|
158 |
+
|
159 |
+
if show_letters == True:
|
160 |
+
return word_ratings, sorted_counts
|
161 |
+
else:
|
162 |
+
return word_ratings
|
163 |
+
|
164 |
+
def count_vows_cons(word: str, y_vow = True):
|
165 |
+
"""
|
166 |
+
Given a passed word, calculate the number of non-unique vowels and consonants in the word (duplicates counted more than once).
|
167 |
+
|
168 |
+
------
|
169 |
+
Parameters:
|
170 |
+
------
|
171 |
+
`word`: str
|
172 |
+
a single passed word (str)
|
173 |
+
`y_vow`: bool
|
174 |
+
if True, "y" is considered a vowel. If False, "y" considered a consonant. Default is True
|
175 |
+
|
176 |
+
------
|
177 |
+
Returns:
|
178 |
+
------
|
179 |
+
`counts`: dict
|
180 |
+
dictionary, where format is {letter type : count}
|
181 |
+
"""
|
182 |
+
|
183 |
+
word = word.lower() # for consistency
|
184 |
+
|
185 |
+
if y_vow == True:
|
186 |
+
vows = "aeiouy"
|
187 |
+
cons = "bcdfghjklmnpqrstvwxz"
|
188 |
+
elif y_vow == False:
|
189 |
+
vows = "aeiou"
|
190 |
+
cons = "bcdfghjklmnpqrstvwxyz"
|
191 |
+
|
192 |
+
counts = {}
|
193 |
+
counts["vows"] = 0
|
194 |
+
counts["cons"] = 0
|
195 |
+
for letter in word:
|
196 |
+
if letter in vows:
|
197 |
+
counts["vows"] += 1
|
198 |
+
if letter in cons:
|
199 |
+
counts["cons"] += 1
|
200 |
+
|
201 |
+
return counts
|
202 |
+
|
203 |
+
def get_word_rating(words_to_rate: list, word_list: list, normalized: bool = True, ascending: bool = False):
|
204 |
+
"""
|
205 |
+
Given a word and a word list, calculates rating each word as a measure of its impact to the next possible guesses in Wordle, ordered according to `reverse` parameter.
|
206 |
+
|
207 |
+
------
|
208 |
+
Parameters:
|
209 |
+
------
|
210 |
+
`words_to_rate`: list
|
211 |
+
list of strings to be rated
|
212 |
+
`word_list`: list
|
213 |
+
list of all possible words (str) of consistent length, to which each word in `words_to_rate` will be compared
|
214 |
+
`normalized`: bool
|
215 |
+
if True, normalizes all ratings on a scale of 0-100, with 100 being the rating for the most optimal word, and 0 for the least optimal word
|
216 |
+
`ascending`: bool
|
217 |
+
if True, returns list ordered ascending. If False, returns list in descending order
|
218 |
+
|
219 |
+
------
|
220 |
+
Returns:
|
221 |
+
------
|
222 |
+
`word_ratings`: list
|
223 |
+
list of tuples. Format is [(word, rating)], where rating is calculated according to above formula
|
224 |
+
`sorted_counts`: list of tuples
|
225 |
+
list of tuples. Format is ("letter", frequency). Sorted according to `sort` value; ["descending" or "ascending"] if passed
|
226 |
+
"""
|
227 |
+
|
228 |
+
if ascending == True:
|
229 |
+
# sorted_counts = get_letter_counts(english_alphabet, word_list, sort = "ascending")
|
230 |
+
sorted_counts = get_letter_counts(word_list = word_list, letters = english_alphabet, sort = "ascending", unique = True)
|
231 |
+
else:
|
232 |
+
# sorted_counts = get_letter_counts(english_alphabet, word_list, sort = "descending")
|
233 |
+
sorted_counts = get_letter_counts(word_list = word_list, letters = english_alphabet, sort = "descending", unique = True)
|
234 |
+
|
235 |
+
all_letters_count = 0
|
236 |
+
for letter, freq in sorted_counts:
|
237 |
+
all_letters_count += freq
|
238 |
+
|
239 |
+
unnormalized_ratings = []
|
240 |
+
for word in words_to_rate:
|
241 |
+
word = word.lower()
|
242 |
+
ratings_dict = {}
|
243 |
+
for letter in word:
|
244 |
+
for freq_letter, freq in sorted_counts:
|
245 |
+
if letter == freq_letter:
|
246 |
+
ratings_dict[letter] = freq
|
247 |
+
|
248 |
+
total_rating = 0
|
249 |
+
for letter, rating in ratings_dict.items():
|
250 |
+
total_rating += rating
|
251 |
+
|
252 |
+
unnormalized_ratings.append((word, round(total_rating / all_letters_count * 100, 2)))
|
253 |
+
|
254 |
+
word_ratings = sorted(unnormalized_ratings, key = operator.itemgetter(1), reverse = True)
|
255 |
+
# # print (word_ratings)
|
256 |
+
|
257 |
+
if normalized == True:
|
258 |
+
if len(word_ratings) > 1:
|
259 |
+
new_tests = []
|
260 |
+
|
261 |
+
for tup in word_ratings:
|
262 |
+
try:
|
263 |
+
normd = round(((tup[1] - word_ratings[-1][1]) / (word_ratings[0][1] - word_ratings[-1][1])) * 100, 2)
|
264 |
+
new_tests.append((tup[0], normd))
|
265 |
+
except:
|
266 |
+
ZeroDivisionError
|
267 |
+
new_tests.append((tup[0], 0.0))
|
268 |
+
|
269 |
+
return new_tests
|
270 |
+
else:
|
271 |
+
return [(word_ratings[0][0], float(100))]
|
272 |
+
|
273 |
+
elif normalized == False:
|
274 |
+
|
275 |
+
return word_ratings
|
276 |
+
|
277 |
+
### Gets most common words of all words of the dataset
|
278 |
+
|
279 |
+
def get_word_distribution(word_list: list, sort: str = "descending"):
|
280 |
+
"""
|
281 |
+
Given a passed str of words and a list of words, produces a frequency distribution of all words
|
282 |
+
|
283 |
+
------
|
284 |
+
Parameters:
|
285 |
+
------
|
286 |
+
`word_list`: list
|
287 |
+
list of words (str) from which word frequencies will be counted
|
288 |
+
`sort`: str
|
289 |
+
if either "descending" or "ascending" are passed, returned list of tuples will be sorted accoringly, else returned dictionary will be unsorted
|
290 |
+
|
291 |
+
------
|
292 |
+
Returns:
|
293 |
+
------
|
294 |
+
`words_counts_dict`: dict
|
295 |
+
dictionary of {word : count} pairs for each word in passed `word_list`
|
296 |
+
`sorted_counts_dicts`: list of tuples
|
297 |
+
list of tuples. Format is ("word", frequency). Ordered according to `sort` values
|
298 |
+
"""
|
299 |
+
|
300 |
+
words_counts_dict = {}
|
301 |
+
|
302 |
+
for word in word_list:
|
303 |
+
if word in words_counts_dict:
|
304 |
+
words_counts_dict[word] += 1
|
305 |
+
else:
|
306 |
+
words_counts_dict[word] = 1
|
307 |
+
|
308 |
+
if sort == "ascending":
|
309 |
+
sorted_counts_dict = (sorted(words_counts_dict.items(), key = operator.itemgetter(1), reverse = False))
|
310 |
+
return sorted_counts_dict
|
311 |
+
|
312 |
+
if sort == "descending":
|
313 |
+
sorted_counts_dict = sorted(words_counts_dict.items(), key = operator.itemgetter(1), reverse = True)
|
314 |
+
return sorted_counts_dict
|
315 |
+
|
316 |
+
############################################################################################################################################################
|
317 |
+
############################################################################################################################################################
|
318 |
+
############################################################################################################################################################
|
319 |
+
############################################################################################################################################################
|
320 |
+
|
321 |
+
## lines 305 - 835
|
322 |
+
|
323 |
+
# def wordle_wizard(word_list: list, max_guesses: int = None,
|
324 |
+
# guess: str = None, target: str = None,
|
325 |
+
# random_guess: bool = False, random_target: bool = False,
|
326 |
+
# verbose: bool = False, drama: float = None,
|
327 |
+
# return_stats: bool = False, record: bool = False):
|
328 |
+
# """
|
329 |
+
# Mimicking the popular web game, this function matches a current word to a target word automatically, in the most statistically optimal way possible.
|
330 |
+
|
331 |
+
# Parameters:
|
332 |
+
# ------
|
333 |
+
# `word_list`: list
|
334 |
+
# list of valid words to be considered
|
335 |
+
# `guess`: str
|
336 |
+
# a string -- must be the same length as `target_word`
|
337 |
+
# `target`: str
|
338 |
+
# a string -- must be the same length as `opening_word`
|
339 |
+
# `max_guesses`: int
|
340 |
+
# the maximum number of attempts allowed to solve the Wordle
|
341 |
+
# `random_guess`: bool
|
342 |
+
# if True, randomly chooses a starting word from all words within `word_list`. If False, passed starting word must be used instead
|
343 |
+
# `random_target`: bool
|
344 |
+
# if True, randomly chooses a target word from all words within `word_list`. If False, passed target word must be used instead
|
345 |
+
# `verbose`: bool
|
346 |
+
# if True, # prints progress and explanation of how function solves the puzzle. If False, # prints only the guessed word at each guess.
|
347 |
+
# `drama`: float or int
|
348 |
+
# if int provided, each guess' output is delayed by that number of seconds, else each output is shown as quickly as possible. For ~dRaMaTiC eFfEcT~
|
349 |
+
# `return_stats`: bool
|
350 |
+
# if True, # prints nothing and returns a dictionary of various statistics about the function's performance trying to solve the puzzle
|
351 |
+
# `record`: bool
|
352 |
+
# if True, creates a .txt file with the same information # printed according to the indicated verbosity
|
353 |
+
|
354 |
+
# Returns:
|
355 |
+
# ------
|
356 |
+
# `stats_dict`: dict
|
357 |
+
# dictionary containing various statistics about the function's performance trying to solve the puzzle
|
358 |
+
# """
|
359 |
+
|
360 |
+
# guess = guess.lower()
|
361 |
+
# target = target.lower()
|
362 |
+
|
363 |
+
# sugg_words = []
|
364 |
+
|
365 |
+
# for i in range(0, 20):
|
366 |
+
# ran_int = random.randint(0, len(word_list) - 1)
|
367 |
+
# word = word_list[ran_int]
|
368 |
+
# sugg_words.append(word)
|
369 |
+
|
370 |
+
# if guess not in word_list:
|
371 |
+
# # print ("Guess word not in passed word list.\nOnly words within the given word list are valid.")
|
372 |
+
# # print (f"Here are some examples of valid words from the passed word list.\n\t{sugg_words[:10]}")
|
373 |
+
# return None
|
374 |
+
|
375 |
+
# if target not in word_list:
|
376 |
+
# # print ("Target word not in passed word list.\nOnly words within the given word list are valid.")
|
377 |
+
# # print (f"Here are some examples of valid words from the passed word list.\n\t{sugg_words[-10:]}")
|
378 |
+
# return None
|
379 |
+
|
380 |
+
# if random_guess == True:
|
381 |
+
# randomint_guess = random.randint(0, len(word_list) - 1)
|
382 |
+
# guess = word_list[randomint_guess]
|
383 |
+
|
384 |
+
# if random_target == True:
|
385 |
+
# randomint_target = random.randint(0, len(word_list) - 1)
|
386 |
+
# target = word_list[randomint_target]
|
387 |
+
|
388 |
+
# stats_dict = {}
|
389 |
+
# stats_dict['first_guess'] = guess
|
390 |
+
# stats_dict['target_word'] = target
|
391 |
+
# stats_dict['first_guess_vowels'] = float(count_vows_cons(guess, y_vow = True)['vows'])
|
392 |
+
# stats_dict['first_guess_consonants'] = float(count_vows_cons(guess, y_vow = True)['cons'])
|
393 |
+
# stats_dict['target_vowels'] = float(count_vows_cons(target, y_vow = True)['vows'])
|
394 |
+
# stats_dict['target_consonants'] = float(count_vows_cons(target, y_vow = True)['cons'])
|
395 |
+
|
396 |
+
# # get rating of the first guess word and target word in the entire word_list
|
397 |
+
# for tup in get_word_rating(word_list, word_list, normalized = True):
|
398 |
+
# if tup[0] == guess:
|
399 |
+
# stats_dict['first_guess_rating'] = tup[1]
|
400 |
+
# if tup[0] == target:
|
401 |
+
# stats_dict['target_rating'] = tup[1]
|
402 |
+
|
403 |
+
# guess_entropies = []
|
404 |
+
# guess_entropies.append(stats_dict['first_guess_rating'])
|
405 |
+
|
406 |
+
# # luck_guess_1 = round(1 - ((1 / len(word_list)) * guess_entropies[0] / 100), 2) * 100
|
407 |
+
|
408 |
+
# english_alphabet = "abcdefghijklmnopqrstuvwxyz"
|
409 |
+
|
410 |
+
# # word_list_sorted_counts = get_letter_counts(english_alphabet, word_list, sort = "descending")
|
411 |
+
# word_list_sorted_counts = get_letter_counts(word_list = word_list, letters = english_alphabet, sort = "descending", unique = True)
|
412 |
+
|
413 |
+
|
414 |
+
# wordlen = len(guess)
|
415 |
+
# letter_positions = set(i for i in range(0, wordlen))
|
416 |
+
|
417 |
+
# guess_set = set()
|
418 |
+
# perfect_dict = {}
|
419 |
+
# wrong_pos_dict = {}
|
420 |
+
# wrong_pos_set = set()
|
421 |
+
# dont_guess_again = set()
|
422 |
+
|
423 |
+
# guessed_words = [] # running set of guessed words
|
424 |
+
# guess_num = 0 # baseline for variable
|
425 |
+
# dont_guess_words = set()
|
426 |
+
# incorrect_positions = []
|
427 |
+
# reduction_per_guess = []
|
428 |
+
|
429 |
+
# if max_guesses == None: # if no value is passed, default is len(guess)
|
430 |
+
# max_guesses = wordlen
|
431 |
+
# else: # else it is the value passed
|
432 |
+
# max_guesses = max_guesses
|
433 |
+
|
434 |
+
# perfect_letts_per_guess = []
|
435 |
+
# wrong_pos_per_guess = []
|
436 |
+
# wrong_letts_per_guess = []
|
437 |
+
|
438 |
+
# while guess: # while there is any guess -- there are conditions to break it at the bottom
|
439 |
+
|
440 |
+
# guess_num += 1
|
441 |
+
|
442 |
+
# guessed_words.append(guess)
|
443 |
+
|
444 |
+
# if drama:
|
445 |
+
# time.sleep(drama)
|
446 |
+
|
447 |
+
# # guess_num += 1 # each time the guess is processed
|
448 |
+
# if return_stats == False:
|
449 |
+
# if guess_num == 1:
|
450 |
+
# print("-----------------------------\n")
|
451 |
+
|
452 |
+
# if guess == target:
|
453 |
+
# stats_dict['target_guessed'] = True
|
454 |
+
# if return_stats == False:
|
455 |
+
# if guess_num == 1:
|
456 |
+
# # print(f"Congratulations! The Wordle has been solved in {guess_num} guess, that's amazingly lucky!")
|
457 |
+
# print(f"The starting word and target word are the same. Try entering two different words to see how the puzzle can be solved.")
|
458 |
+
# # print(f"The target word was {target}")
|
459 |
+
|
460 |
+
|
461 |
+
# perfect_letts_per_guess.append(5)
|
462 |
+
# wrong_pos_per_guess.append(0)
|
463 |
+
# wrong_letts_per_guess.append(0)
|
464 |
+
# break
|
465 |
+
|
466 |
+
# if return_stats == False:
|
467 |
+
# print(f"**Guess {guess_num}: '{guess}'**")
|
468 |
+
|
469 |
+
# guess_set = set()
|
470 |
+
# wrong_pos_set = set()
|
471 |
+
|
472 |
+
# #### Step 2 -- ALL PERFECT
|
473 |
+
# for i in letter_positions: # number of letters in each word (current word and target word)
|
474 |
+
# guess_set.add(guess[i])
|
475 |
+
|
476 |
+
# if guess[i] not in perfect_dict:
|
477 |
+
# perfect_dict[guess[i]] = set()
|
478 |
+
# if guess[i] not in wrong_pos_dict:
|
479 |
+
# wrong_pos_dict[guess[i]] = set()
|
480 |
+
|
481 |
+
# ### EVALUATE CURRENT GUESS
|
482 |
+
# if guess[i] == target[i]: # letter == correct and position == correct
|
483 |
+
# perfect_dict[guess[i]].add(i)
|
484 |
+
|
485 |
+
# if (guess[i] != target[i] and guess[i] in target): # letter == correct and position != correct
|
486 |
+
# wrong_pos_dict[guess[i]].add(i)
|
487 |
+
# wrong_pos_set.add(guess[i])
|
488 |
+
|
489 |
+
# if guess[i] not in target: # if letter is not relevant at all
|
490 |
+
# dont_guess_again.add(guess[i])
|
491 |
+
|
492 |
+
# #### Step 3 -- ALL PERFECT
|
493 |
+
# next_letters = set()
|
494 |
+
# for letter, positions in perfect_dict.items():
|
495 |
+
# if len(positions) > 0:
|
496 |
+
# next_letters.add(letter)
|
497 |
+
|
498 |
+
# for letter, positions in wrong_pos_dict.items():
|
499 |
+
# if len(positions) > 0:
|
500 |
+
# next_letters.add(letter)
|
501 |
+
|
502 |
+
# #### List of tuples of correct letter positions in new valid words. Eg: [('e', 2), ('a', 3)]
|
503 |
+
# perfect_letters = []
|
504 |
+
# for letter, positions in perfect_dict.items():
|
505 |
+
# for pos in positions:
|
506 |
+
# if len(positions) > 0:
|
507 |
+
# perfect_letters.append((letter, pos))
|
508 |
+
|
509 |
+
# #### all words that have correct letters in same spots
|
510 |
+
# words_matching_correct_all = []
|
511 |
+
# for word in word_list:
|
512 |
+
# word_set = set()
|
513 |
+
# for letter, pos in perfect_letters:
|
514 |
+
# if pos < len(word):
|
515 |
+
# if word[pos] == letter:
|
516 |
+
# words_matching_correct_all.append(word)
|
517 |
+
|
518 |
+
# #### excluding words with letters in known incorrect positions
|
519 |
+
# for letter, positions in wrong_pos_dict.items():
|
520 |
+
# for pos in positions:
|
521 |
+
# if len(positions) > 0:
|
522 |
+
# if (letter, pos) not in incorrect_positions:
|
523 |
+
# incorrect_positions.append((letter, pos))
|
524 |
+
|
525 |
+
# # sorting lists of tuples just to make them look nice in the # printout
|
526 |
+
# incorrect_positions = sorted(incorrect_positions, key = operator.itemgetter(1), reverse = False)
|
527 |
+
# perfect_letters = sorted(perfect_letters, key = operator.itemgetter(1), reverse = False)
|
528 |
+
|
529 |
+
# #### all words that have correct letters in incorrect spots -- so they can be excluded efficiently
|
530 |
+
|
531 |
+
# # print(incorrect_positions)
|
532 |
+
|
533 |
+
# for word in word_list:
|
534 |
+
# word_set = set()
|
535 |
+
# for letter, pos in incorrect_positions:
|
536 |
+
# if pos < len(word):
|
537 |
+
# if word[pos] == letter:
|
538 |
+
# dont_guess_words.add(word)
|
539 |
+
# for word in word_list:
|
540 |
+
# word_set = set()
|
541 |
+
# for letter, pos in incorrect_positions:
|
542 |
+
# if pos < len(word):
|
543 |
+
# if word[pos] == letter:
|
544 |
+
# dont_guess_words.add(word)
|
545 |
+
|
546 |
+
# for bad_letter in dont_guess_again:
|
547 |
+
# for word in word_list:
|
548 |
+
# if (bad_letter in word and word not in dont_guess_words):
|
549 |
+
# dont_guess_words.add(word)
|
550 |
+
|
551 |
+
# if return_stats == False:
|
552 |
+
# if verbose == True:
|
553 |
+
# print(f"Letters in correct positions:\n\t{perfect_letters}\n")
|
554 |
+
# print(f"Letters in incorrect positions:\n\t{incorrect_positions}\n")
|
555 |
+
# # print (f"Letters to guess again:\n\t{sorted(list(next_letters), reverse = False)}\n")
|
556 |
+
# print(f"Letters to not guess again:\n\t{sorted(list(dont_guess_again), reverse = False)}\n") # works
|
557 |
+
|
558 |
+
# # Returns True
|
559 |
+
# # print(A.issubset(B)) # "if everything in A is in B", returns Bool
|
560 |
+
|
561 |
+
# perfect_letts_per_guess.append(len(perfect_letters))
|
562 |
+
# wrong_pos_per_guess.append(len(incorrect_positions))
|
563 |
+
# wrong_letts_per_guess.append(len(dont_guess_again))
|
564 |
+
|
565 |
+
# potential_next_guesses = set()
|
566 |
+
# middle_set = set()
|
567 |
+
|
568 |
+
# if len(perfect_letters) == 0 and len(incorrect_positions) == 0: # if there are NEITHER perfect letters, NOR incorrect positions, ....
|
569 |
+
# for word in word_list:
|
570 |
+
# if word not in dont_guess_words:
|
571 |
+
# if word not in guessed_words:
|
572 |
+
# potential_next_guesses.add(word)
|
573 |
+
|
574 |
+
# # print(f"GUESS {guess_num} : TEST 1-1")
|
575 |
+
|
576 |
+
# if len(perfect_letters) == 0 and len(incorrect_positions) != 0: # if there are no perfect letters whatsoever, but there ARE incorrect positions ....
|
577 |
+
# for word in word_list:
|
578 |
+
# for incor_letter, incor_pos in incorrect_positions:
|
579 |
+
# if incor_pos < len(word):
|
580 |
+
# if word[incor_pos] != incor_letter:
|
581 |
+
# if word not in dont_guess_words: # just in case
|
582 |
+
# word_set = set()
|
583 |
+
# for letter in word:
|
584 |
+
# word_set.add(letter)
|
585 |
+
|
586 |
+
# if next_letters.issubset(word_set):
|
587 |
+
# if word not in guessed_words:
|
588 |
+
# if len(dont_guess_again) > 0:
|
589 |
+
# for bad_letter in dont_guess_again:
|
590 |
+
# if bad_letter not in word:
|
591 |
+
# # potential_next_guesses.append(word)
|
592 |
+
# potential_next_guesses.add(word)
|
593 |
+
# else:
|
594 |
+
# potential_next_guesses.add(word)
|
595 |
+
|
596 |
+
# # print(f"GUESS {guess_num} : TEST 2-1")
|
597 |
+
|
598 |
+
# else:
|
599 |
+
# for word in word_list:
|
600 |
+
# if word not in dont_guess_words: # just in case
|
601 |
+
# word_set = set()
|
602 |
+
# for letter in word:
|
603 |
+
# word_set.add(letter)
|
604 |
+
# if next_letters.issubset(word_set):
|
605 |
+
# if word not in guessed_words:
|
606 |
+
# # # print ("TEST 3-2")
|
607 |
+
|
608 |
+
# if len(dont_guess_again) > 0:
|
609 |
+
# for bad_letter in dont_guess_again:
|
610 |
+
# if bad_letter not in word:
|
611 |
+
# middle_set.add(word)
|
612 |
+
# else:
|
613 |
+
# middle_set.add(word)
|
614 |
+
# for word in middle_set:
|
615 |
+
# dummy_list = []
|
616 |
+
# for good_lett, good_pos in perfect_letters:
|
617 |
+
# if word[good_pos] == good_lett:
|
618 |
+
# dummy_list.append(1)
|
619 |
+
# if len(dummy_list) == len(perfect_letters):
|
620 |
+
# potential_next_guesses.add(word)
|
621 |
+
# for word in middle_set:
|
622 |
+
# dummy_list = []
|
623 |
+
# for bad_lett, bad_pos in incorrect_positions:
|
624 |
+
# if bad_pos < len(word):
|
625 |
+
# if word[bad_pos] == bad_lett:
|
626 |
+
# dummy_list.append(1)
|
627 |
+
# if len(dummy_list) > 0:
|
628 |
+
# potential_next_guesses.remove(word)
|
629 |
+
|
630 |
+
# # print(f"GUESS {guess_num} : TEST 3-1")
|
631 |
+
|
632 |
+
# if return_stats == False:
|
633 |
+
# if verbose == True:
|
634 |
+
# if len(potential_next_guesses) > 1:
|
635 |
+
# # print(f"At this point:")
|
636 |
+
# print(f"\t{len(word_list) - len(potential_next_guesses)}, {round((len(word_list) - len(potential_next_guesses)) / len(word_list) * 100, 2)}% of total words have been eliminated, and")
|
637 |
+
# print(f"\t{len(potential_next_guesses)}, {round(len(potential_next_guesses) / len(word_list) * 100, 2)}% of total words remain possible.\n")
|
638 |
+
|
639 |
+
# else:
|
640 |
+
# # print(f"At this point:")
|
641 |
+
# print(f"\t{len(word_list) - len(potential_next_guesses)}, {round((len(word_list) - len(potential_next_guesses)) / len(word_list) * 100, 2)}% of total words have been eliminated, and")
|
642 |
+
# print(f"\t{len(potential_next_guesses)}, {round(len(potential_next_guesses) / len(word_list) * 100, 2)}% of total words remain possible.\n")
|
643 |
+
|
644 |
+
# reduction_per_guess.append(len(potential_next_guesses))
|
645 |
+
|
646 |
+
# #### Guessing next word
|
647 |
+
# if len(potential_next_guesses) == 1:
|
648 |
+
|
649 |
+
# if return_stats == False:
|
650 |
+
# if verbose == True:
|
651 |
+
# print(f"All potential next guesses:\n\t{get_word_rating(words_to_rate = list(potential_next_guesses), word_list = word_list)}\n")
|
652 |
+
# print(f"Words guessed so far:\n\t{guessed_words}.\n")
|
653 |
+
|
654 |
+
# print(f"The only remaining possible word is:\n\t'{list(potential_next_guesses)[0]}'\n")
|
655 |
+
|
656 |
+
# guess = list(potential_next_guesses)[0]
|
657 |
+
# guess_entropies.append(get_word_rating([guess], word_list, normalized = False, ascending = False)[0][1])
|
658 |
+
|
659 |
+
# else:
|
660 |
+
|
661 |
+
# best_next_guesses = list(potential_next_guesses)
|
662 |
+
# # # print (best_next_guesses)
|
663 |
+
# word_ratings = get_word_rating(best_next_guesses, word_list, normalized = False, ascending = False) # "internal" ratings
|
664 |
+
|
665 |
+
# # Get max rating of all words
|
666 |
+
# max_rating = -np.inf
|
667 |
+
# for word, rating in word_ratings:
|
668 |
+
# if rating > max_rating:
|
669 |
+
# max_rating = rating
|
670 |
+
|
671 |
+
# # add best rated words (all equally best rating in next guess list) to set
|
672 |
+
# best_of_the_best_1 = []
|
673 |
+
# for word, rating in word_ratings:
|
674 |
+
# if rating == max_rating:
|
675 |
+
# best_of_the_best_1.append(word)
|
676 |
+
|
677 |
+
# # only using top ten most frequent prefixes suffixes to bias. After that it the impact is especially negligible
|
678 |
+
# test_starts = get_gram_freq(word_list = word_list, letters_length = 1, position = "start", search = None)[:10]
|
679 |
+
# test_ends = get_gram_freq(word_list = word_list, letters_length = 1, position = "end", search = None)[:10]
|
680 |
+
|
681 |
+
# # list of the best words that also have the most frequent starting and ending letters (suffixes and prefixes didn't have an impact)
|
682 |
+
# best_of_the_best_2 = []
|
683 |
+
# for start_gram, start_count in test_starts:
|
684 |
+
# for end_gram, end_count in test_ends:
|
685 |
+
# for word in best_of_the_best_1:
|
686 |
+
# if word[:1] == start_gram and word[-1:] == end_gram:
|
687 |
+
# best_of_the_best_2.append(word)
|
688 |
+
|
689 |
+
# if len(best_of_the_best_2) > 0:
|
690 |
+
# guess = best_of_the_best_2[0]
|
691 |
+
# else:
|
692 |
+
# guess = best_of_the_best_1[0] # they're all equally the best of the best possible guesses so just pick the first
|
693 |
+
|
694 |
+
# # guess_entropies.append(get_word_rating([guess], word_list, normalized = False, ascending = False)[0][1])
|
695 |
+
|
696 |
+
# if return_stats == False:
|
697 |
+
# if verbose == True:
|
698 |
+
# if len(word_ratings) <= 40:
|
699 |
+
# print(f"All potential next guesses:\n\t{word_ratings}\n")
|
700 |
+
# print(f"Words guessed so far:\n\t{guessed_words}.\n")
|
701 |
+
# else:
|
702 |
+
# print(f"The top 40 potential next guesses are:\n\t{word_ratings[:40]}\n")
|
703 |
+
# print(f"Words guessed so far:\n\t{guessed_words}.\n")
|
704 |
+
|
705 |
+
# guess_entropies.append(get_word_rating([guess], word_list, normalized = False, ascending = False)[0][1])
|
706 |
+
|
707 |
+
# #### Guess has now been made -- what to do next
|
708 |
+
# if guess_num == max_guesses: # if at max guesses allowed
|
709 |
+
# guessed_words.append(guess)
|
710 |
+
# stats_dict['target_guessed'] = False
|
711 |
+
# if return_stats == False:
|
712 |
+
# if verbose == True:
|
713 |
+
# print("-----------------------------\n")
|
714 |
+
# print(f"Unfortunately, the Wordle could not be solved in {max_guesses} guesses.\n")
|
715 |
+
# print(f"The target word was '{target}'.\n")
|
716 |
+
# print("-----------------------------\n")
|
717 |
+
# else:
|
718 |
+
# print(f"\nUnfortunately, Wordle Wizard couldn't solve the puzzle in {max_guesses} guesses. Could you?")
|
719 |
+
# print(f"The target word was '{target}'.\n")
|
720 |
+
# break
|
721 |
+
# else: # if not at max guesses yet allowed
|
722 |
+
# # stats_dict['target_guessed'] = False
|
723 |
+
# if return_stats == False:
|
724 |
+
# if verbose == True:
|
725 |
+
# print(f"Next guess:\n\t'{guess}'")
|
726 |
+
# print("\n-----------------------------\n")
|
727 |
+
|
728 |
+
# if guess == target:
|
729 |
+
# guess_num += 1
|
730 |
+
# guessed_words.append(guess)
|
731 |
+
# stats_dict['target_guessed'] = True
|
732 |
+
|
733 |
+
# if return_stats == False:
|
734 |
+
# print(f"**Guess {guess_num}: '{guess}'**\n")
|
735 |
+
# print(f"Wordle Wizard has solved the puzzle in {guess_num} guesses!")
|
736 |
+
|
737 |
+
# if max_guesses - guess_num == 1:
|
738 |
+
# print(f"There was only {max_guesses - guess_num} guess remaining.")
|
739 |
+
# else:
|
740 |
+
# print(f"There were still {max_guesses - guess_num} guesses remaining.")
|
741 |
+
|
742 |
+
# if return_stats == False:
|
743 |
+
# # stats_dict['target_guessed'] = True
|
744 |
+
# print(f"\nThe target word was **'{target}'**.")
|
745 |
+
# print("\n-----------------------------")
|
746 |
+
# break
|
747 |
+
|
748 |
+
# #### STATS STUFF
|
749 |
+
# mid_guesses_vows = 0
|
750 |
+
# mid_guesses_cons = 0
|
751 |
+
# avg_perf_letters = 0
|
752 |
+
# avg_wrong_pos_letters = 0
|
753 |
+
# avg_wrong_letters = 0
|
754 |
+
|
755 |
+
# for i, word in enumerate(guessed_words):
|
756 |
+
# mid_guesses_vows += count_vows_cons(word, y_vow = True)['vows']
|
757 |
+
# mid_guesses_cons += count_vows_cons(word, y_vow = True)['cons']
|
758 |
+
|
759 |
+
# for i in range(0, len(guessed_words) - 1):
|
760 |
+
# avg_perf_letters += perfect_letts_per_guess[i]
|
761 |
+
# avg_wrong_pos_letters += wrong_pos_per_guess[i]
|
762 |
+
# avg_wrong_letters += wrong_letts_per_guess[i]
|
763 |
+
|
764 |
+
# stats_dict['mid_guesses_avg_vows'] = float(round(mid_guesses_vows / len(guessed_words), 2))
|
765 |
+
# stats_dict['mid_guesses_avg_cons'] = float(round(mid_guesses_cons / len(guessed_words), 2))
|
766 |
+
|
767 |
+
# stats_dict['avg_perf_letters'] = float(round(np.mean(avg_perf_letters), 2))
|
768 |
+
# stats_dict['avg_wrong_pos_letters'] = float(round(np.mean(avg_wrong_pos_letters), 2))
|
769 |
+
# stats_dict['avg_wrong_letters'] = float(round(np.mean(avg_wrong_letters), 2))
|
770 |
+
|
771 |
+
# # average number of words remaining after each guess -- the higher this is, the luckier the person got (the lower, the more guesses it took)
|
772 |
+
# stats_dict['avg_remaining'] = float(round(np.mean(reduction_per_guess), 2))
|
773 |
+
|
774 |
+
# # avg rating of each guessed word relative to all other words possible at that moment -- this should consistently be 100 for the algorithm, but will be different for user
|
775 |
+
# if len(guess_entropies) > 1: # in case of guessing it correctly on the first try
|
776 |
+
# sum_entropies = 0
|
777 |
+
# for rating in guess_entropies:
|
778 |
+
# sum_entropies += rating
|
779 |
+
|
780 |
+
# average_rating = float(round(sum_entropies / len(guess_entropies), 2))
|
781 |
+
# stats_dict['avg_intermediate_guess_rating'] = average_rating
|
782 |
+
# else:
|
783 |
+
# stats_dict['avg_intermediate_guess_rating'] = float(100)
|
784 |
+
|
785 |
+
# expected_guesses = 3.85
|
786 |
+
|
787 |
+
# # guess_num = 3
|
788 |
+
# # average_rating = 95
|
789 |
+
# luck = round(1 - ((((guess_num / expected_guesses) * (stats_dict['avg_intermediate_guess_rating'] / 100)) / max_guesses) * 5), 2)
|
790 |
+
# stats_dict['luck'] = luck
|
791 |
+
|
792 |
+
# if record == True:
|
793 |
+
# if verbose == True:
|
794 |
+
# with open(f"solutions/{guessed_words[0]}_{target}_wizard_detailed.txt", "w") as fout:
|
795 |
+
|
796 |
+
# fout.write(line + "\n") # write each line of list of # printed text to .txt file
|
797 |
+
# else:
|
798 |
+
# with open(f"solutions/{guessed_words[0]}_{target}_wizard_summary.txt", "w") as fout:
|
799 |
+
|
800 |
+
# fout.write(line + "\n") # write
|
801 |
+
|
802 |
+
# # if guess_num <= len(guess):
|
803 |
+
# if guess_num <= 6:
|
804 |
+
# stats_dict['valid_success'] = True
|
805 |
+
# else:
|
806 |
+
# stats_dict['valid_success'] = False
|
807 |
+
|
808 |
+
# stats_dict['num_guesses'] = float(guess_num)
|
809 |
+
|
810 |
+
# # if return_stats == True:
|
811 |
+
# # return stats_dict
|
812 |
+
def wordle_wizard_cheat(guesses: list, word_list: list, max_guesses: int = None,
|
813 |
+
target: str = None,
|
814 |
+
random_guess: bool = False, random_target: bool = False,
|
815 |
+
verbose: bool = False, drama: float = None,
|
816 |
+
return_stats: bool = False, record: bool = False):
|
817 |
+
"""
|
818 |
+
Mimicking the popular web game, this function matches a current word to a target word automatically, in the most statistically optimal way possible.
|
819 |
+
|
820 |
+
Parameters:
|
821 |
+
------
|
822 |
+
`word_list`: list
|
823 |
+
list of valid words to be considered
|
824 |
+
`guess`: str
|
825 |
+
a string -- must be the same length as `target_word`
|
826 |
+
`target`: str
|
827 |
+
a string -- must be the same length as `opening_word`
|
828 |
+
`max_guesses`: int
|
829 |
+
the maximum number of attempts allowed to solve the Wordle
|
830 |
+
`random_guess`: bool
|
831 |
+
if True, randomly chooses a starting word from all words within `word_list`. If False, passed starting word must be used instead
|
832 |
+
`random_target`: bool
|
833 |
+
if True, randomly chooses a target word from all words within `word_list`. If False, passed target word must be used instead
|
834 |
+
`verbose`: bool
|
835 |
+
if True, # st.writes progress and explanation of how function solves the puzzle. If False, # st.writes only the guessed word at each guess.
|
836 |
+
`drama`: float or int
|
837 |
+
if int provided, each guess' output is delayed by that number of seconds, else each output is shown as quickly as possible. For ~dRaMaTiC eFfEcT~
|
838 |
+
`return_stats`: bool
|
839 |
+
if True, # st.writes nothing and returns a dictionary of various statistics about the function's performance trying to solve the puzzle
|
840 |
+
`record`: bool
|
841 |
+
if True, creates a .txt file with the same information # st.writeed according to the indicated verbosity
|
842 |
+
|
843 |
+
Returns:
|
844 |
+
------
|
845 |
+
`stats_dict`: dict
|
846 |
+
dictionary containing various statistics about the function's performance trying to solve the puzzle
|
847 |
+
"""
|
848 |
+
|
849 |
+
# guess = guess.lower()
|
850 |
+
target = target.lower()
|
851 |
+
|
852 |
+
if target not in word_list:
|
853 |
+
word_list.append(target)
|
854 |
+
|
855 |
+
sugg_words = []
|
856 |
+
|
857 |
+
for i in range(0, 20):
|
858 |
+
ran_int = random.randint(0, len(word_list) - 1)
|
859 |
+
word = word_list[ran_int]
|
860 |
+
sugg_words.append(word)
|
861 |
+
|
862 |
+
guess = guesses[0]
|
863 |
+
|
864 |
+
stats_dict = {}
|
865 |
+
stats_dict['first_guess'] = guess
|
866 |
+
stats_dict['target_word'] = target
|
867 |
+
stats_dict['first_guess_vowels'] = float(count_vows_cons(guess, y_vow = True)['vows'])
|
868 |
+
stats_dict['first_guess_consonants'] = float(count_vows_cons(guess, y_vow = True)['cons'])
|
869 |
+
stats_dict['target_vowels'] = float(count_vows_cons(target, y_vow = True)['vows'])
|
870 |
+
stats_dict['target_consonants'] = float(count_vows_cons(target, y_vow = True)['cons'])
|
871 |
+
|
872 |
+
# get rating of the first guess word and target word in the entire word_list
|
873 |
+
for tup in get_word_rating(word_list, word_list, normalized = True):
|
874 |
+
if tup[0] == guess:
|
875 |
+
stats_dict['first_guess_rating'] = tup[1]
|
876 |
+
if tup[0] == target:
|
877 |
+
stats_dict['target_rating'] = tup[1]
|
878 |
+
|
879 |
+
guess_entropies = []
|
880 |
+
guess_entropies.append(stats_dict['first_guess_rating'])
|
881 |
+
|
882 |
+
# luck_guess_1 = round(1 - ((1 / len(word_list)) * guess_entropies[0] / 100), 2) * 100
|
883 |
+
|
884 |
+
english_alphabet = "abcdefghijklmnopqrstuvwxyz"
|
885 |
+
|
886 |
+
# word_list_sorted_counts = get_letter_counts(english_alphabet, word_list, sort = "descending")
|
887 |
+
word_list_sorted_counts = get_letter_counts(word_list = word_list, letters = english_alphabet, sort = "descending", unique = True)
|
888 |
+
|
889 |
+
wordlen = len(guesses[0])
|
890 |
+
letter_positions = set(i for i in range(0, wordlen))
|
891 |
+
|
892 |
+
guess_set = set()
|
893 |
+
perfect_dict = {}
|
894 |
+
wrong_pos_dict = {}
|
895 |
+
wrong_pos_set = set()
|
896 |
+
dont_guess_again = set()
|
897 |
+
|
898 |
+
guessed_words = [] # running set of guessed words
|
899 |
+
guess_num = 0 # baseline for variable
|
900 |
+
dont_guess_words = set()
|
901 |
+
incorrect_positions = []
|
902 |
+
reduction_per_guess = []
|
903 |
+
|
904 |
+
if max_guesses == None: # if no value is passed, default is len(guess)
|
905 |
+
max_guesses = wordlen
|
906 |
+
else: # else it is the value passed
|
907 |
+
max_guesses = max_guesses
|
908 |
+
|
909 |
+
perfect_letts_per_guess = []
|
910 |
+
wrong_pos_per_guess = []
|
911 |
+
wrong_letts_per_guess = []
|
912 |
+
|
913 |
+
# while guess: # while there is any guess -- there are conditions to break it at the bottom
|
914 |
+
|
915 |
+
for guess_num, guess in enumerate(guesses):
|
916 |
+
|
917 |
+
guess_num += 1
|
918 |
+
|
919 |
+
guessed_words.append(guess)
|
920 |
+
|
921 |
+
if drama:
|
922 |
+
time.sleep(drama)
|
923 |
+
|
924 |
+
# guess_num += 1 # each time the guess is processed
|
925 |
+
if return_stats == False:
|
926 |
+
if guess_num == 1:
|
927 |
+
st.write("-----------------------------\n")
|
928 |
+
|
929 |
+
if guess == target:
|
930 |
+
stats_dict['target_guessed'] = True
|
931 |
+
if return_stats == False:
|
932 |
+
if guess_num == 1:
|
933 |
+
# st.write(f"Congratulations! The Wordle has been solved in {guess_num} guess, that's amazingly lucky!")
|
934 |
+
st.write(f"The starting word and target word are the same. Try entering two different words to see how the puzzle can be solved.")
|
935 |
+
# st.write(f"The target word was {target}")
|
936 |
+
|
937 |
+
|
938 |
+
perfect_letts_per_guess.append(5)
|
939 |
+
wrong_pos_per_guess.append(0)
|
940 |
+
wrong_letts_per_guess.append(0)
|
941 |
+
break
|
942 |
+
|
943 |
+
if return_stats == False:
|
944 |
+
st.write(f"**Guess {guess_num}: '{guess}'**")
|
945 |
+
|
946 |
+
guess_set = set()
|
947 |
+
wrong_pos_set = set()
|
948 |
+
|
949 |
+
#### Step 2 -- ALL PERFECT
|
950 |
+
for i in letter_positions: # number of letters in each word (current word and target word)
|
951 |
+
guess_set.add(guess[i])
|
952 |
+
|
953 |
+
if guess[i] not in perfect_dict:
|
954 |
+
perfect_dict[guess[i]] = set()
|
955 |
+
if guess[i] not in wrong_pos_dict:
|
956 |
+
wrong_pos_dict[guess[i]] = set()
|
957 |
+
|
958 |
+
### EVALUATE CURRENT GUESS
|
959 |
+
if guess[i] == target[i]: # letter == correct and position == correct
|
960 |
+
perfect_dict[guess[i]].add(i)
|
961 |
+
|
962 |
+
if (guess[i] != target[i] and guess[i] in target): # letter == correct and position != correct
|
963 |
+
wrong_pos_dict[guess[i]].add(i)
|
964 |
+
wrong_pos_set.add(guess[i])
|
965 |
+
|
966 |
+
if guess[i] not in target: # if letter is not relevant at all
|
967 |
+
dont_guess_again.add(guess[i])
|
968 |
+
|
969 |
+
#### Step 3 -- ALL PERFECT
|
970 |
+
next_letters = set()
|
971 |
+
for letter, positions in perfect_dict.items():
|
972 |
+
if len(positions) > 0:
|
973 |
+
next_letters.add(letter)
|
974 |
+
|
975 |
+
for letter, positions in wrong_pos_dict.items():
|
976 |
+
if len(positions) > 0:
|
977 |
+
next_letters.add(letter)
|
978 |
+
|
979 |
+
#### List of tuples of correct letter positions in new valid words. Eg: [('e', 2), ('a', 3)]
|
980 |
+
perfect_letters = []
|
981 |
+
for letter, positions in perfect_dict.items():
|
982 |
+
for pos in positions:
|
983 |
+
if len(positions) > 0:
|
984 |
+
perfect_letters.append((letter, pos))
|
985 |
+
|
986 |
+
#### all words that have correct letters in same spots
|
987 |
+
words_matching_correct_all = []
|
988 |
+
for word in word_list:
|
989 |
+
word_set = set()
|
990 |
+
for letter, pos in perfect_letters:
|
991 |
+
if pos < len(word):
|
992 |
+
if word[pos] == letter:
|
993 |
+
words_matching_correct_all.append(word)
|
994 |
+
|
995 |
+
#### excluding words with letters in known incorrect positions
|
996 |
+
for letter, positions in wrong_pos_dict.items():
|
997 |
+
for pos in positions:
|
998 |
+
if len(positions) > 0:
|
999 |
+
if (letter, pos) not in incorrect_positions:
|
1000 |
+
incorrect_positions.append((letter, pos))
|
1001 |
+
|
1002 |
+
# sorting lists of tuples just to make them look nice in the # st.writeout
|
1003 |
+
incorrect_positions = sorted(incorrect_positions, key = operator.itemgetter(1), reverse = False)
|
1004 |
+
perfect_letters = sorted(perfect_letters, key = operator.itemgetter(1), reverse = False)
|
1005 |
+
|
1006 |
+
#### all words that have correct letters in incorrect spots -- so they can be excluded efficiently
|
1007 |
+
|
1008 |
+
# st.write(incorrect_positions)
|
1009 |
+
|
1010 |
+
for word in word_list:
|
1011 |
+
word_set = set()
|
1012 |
+
for letter, pos in incorrect_positions:
|
1013 |
+
if pos < len(word):
|
1014 |
+
if word[pos] == letter:
|
1015 |
+
dont_guess_words.add(word)
|
1016 |
+
for word in word_list:
|
1017 |
+
word_set = set()
|
1018 |
+
for letter, pos in incorrect_positions:
|
1019 |
+
if pos < len(word):
|
1020 |
+
if word[pos] == letter:
|
1021 |
+
dont_guess_words.add(word)
|
1022 |
+
|
1023 |
+
for bad_letter in dont_guess_again:
|
1024 |
+
for word in word_list:
|
1025 |
+
if (bad_letter in word and word not in dont_guess_words):
|
1026 |
+
dont_guess_words.add(word)
|
1027 |
+
|
1028 |
+
if return_stats == False:
|
1029 |
+
if verbose == True:
|
1030 |
+
st.write(f"Letters in correct positions:\n\t{perfect_letters}\n")
|
1031 |
+
st.write(f"Letters in incorrect positions:\n\t{incorrect_positions}\n")
|
1032 |
+
# st.write (f"Letters to guess again:\n\t{sorted(list(next_letters), reverse = False)}\n")
|
1033 |
+
st.write(f"Letters to not guess again:\n\t{sorted(list(dont_guess_again), reverse = False)}\n") # works
|
1034 |
+
|
1035 |
+
# Returns True
|
1036 |
+
# st.write(A.issubset(B)) # "if everything in A is in B", returns Bool
|
1037 |
+
|
1038 |
+
perfect_letts_per_guess.append(len(perfect_letters))
|
1039 |
+
wrong_pos_per_guess.append(len(incorrect_positions))
|
1040 |
+
wrong_letts_per_guess.append(len(dont_guess_again))
|
1041 |
+
|
1042 |
+
potential_next_guesses = set()
|
1043 |
+
middle_set = set()
|
1044 |
+
|
1045 |
+
if len(perfect_letters) == 0 and len(incorrect_positions) == 0: # if there are NEITHER perfect letters, NOR incorrect positions, ....
|
1046 |
+
for word in word_list:
|
1047 |
+
if word not in dont_guess_words:
|
1048 |
+
if word not in guessed_words:
|
1049 |
+
potential_next_guesses.add(word)
|
1050 |
+
|
1051 |
+
# st.write(f"GUESS {guess_num} : TEST 1-1")
|
1052 |
+
|
1053 |
+
if len(perfect_letters) == 0 and len(incorrect_positions) != 0: # if there are no perfect letters whatsoever, but there ARE incorrect positions ....
|
1054 |
+
for word in word_list:
|
1055 |
+
for incor_letter, incor_pos in incorrect_positions:
|
1056 |
+
if incor_pos < len(word):
|
1057 |
+
if word[incor_pos] != incor_letter:
|
1058 |
+
if word not in dont_guess_words: # just in case
|
1059 |
+
word_set = set()
|
1060 |
+
for letter in word:
|
1061 |
+
word_set.add(letter)
|
1062 |
+
|
1063 |
+
if next_letters.issubset(word_set):
|
1064 |
+
if word not in guessed_words:
|
1065 |
+
if len(dont_guess_again) > 0:
|
1066 |
+
for bad_letter in dont_guess_again:
|
1067 |
+
if bad_letter not in word:
|
1068 |
+
# potential_next_guesses.append(word)
|
1069 |
+
potential_next_guesses.add(word)
|
1070 |
+
else:
|
1071 |
+
potential_next_guesses.add(word)
|
1072 |
+
|
1073 |
+
# st.write(f"GUESS {guess_num} : TEST 2-1")
|
1074 |
+
|
1075 |
+
else:
|
1076 |
+
for word in word_list:
|
1077 |
+
if word not in dont_guess_words: # just in case
|
1078 |
+
word_set = set()
|
1079 |
+
for letter in word:
|
1080 |
+
word_set.add(letter)
|
1081 |
+
if next_letters.issubset(word_set):
|
1082 |
+
if word not in guessed_words:
|
1083 |
+
# # st.write ("TEST 3-2")
|
1084 |
+
|
1085 |
+
if len(dont_guess_again) > 0:
|
1086 |
+
for bad_letter in dont_guess_again:
|
1087 |
+
if bad_letter not in word:
|
1088 |
+
middle_set.add(word)
|
1089 |
+
else:
|
1090 |
+
middle_set.add(word)
|
1091 |
+
for word in middle_set:
|
1092 |
+
dummy_list = []
|
1093 |
+
for good_lett, good_pos in perfect_letters:
|
1094 |
+
if word[good_pos] == good_lett:
|
1095 |
+
dummy_list.append(1)
|
1096 |
+
if len(dummy_list) == len(perfect_letters):
|
1097 |
+
potential_next_guesses.add(word)
|
1098 |
+
for word in middle_set:
|
1099 |
+
dummy_list = []
|
1100 |
+
for bad_lett, bad_pos in incorrect_positions:
|
1101 |
+
if bad_pos < len(word):
|
1102 |
+
if word[bad_pos] == bad_lett:
|
1103 |
+
dummy_list.append(1)
|
1104 |
+
if len(dummy_list) > 0:
|
1105 |
+
potential_next_guesses.remove(word)
|
1106 |
+
|
1107 |
+
# st.write(f"GUESS {guess_num} : TEST 3-1")
|
1108 |
+
|
1109 |
+
if return_stats == False:
|
1110 |
+
if verbose == True:
|
1111 |
+
if len(potential_next_guesses) > 1:
|
1112 |
+
# st.write(f"At this point:")
|
1113 |
+
st.write(f"\t{len(word_list) - len(potential_next_guesses)}, {round((len(word_list) - len(potential_next_guesses)) / len(word_list) * 100, 2)}% of total words have been eliminated, and")
|
1114 |
+
st.write(f"\t{len(potential_next_guesses)}, {round(len(potential_next_guesses) / len(word_list) * 100, 2)}% of total words remain possible.\n")
|
1115 |
+
|
1116 |
+
else:
|
1117 |
+
# st.write(f"At this point:")
|
1118 |
+
st.write(f"\t{len(word_list) - len(potential_next_guesses)}, {round((len(word_list) - len(potential_next_guesses)) / len(word_list) * 100, 2)}% of total words have been eliminated, and")
|
1119 |
+
st.write(f"\t{len(potential_next_guesses)}, {round(len(potential_next_guesses) / len(word_list) * 100, 2)}% of total words remain possible.\n")
|
1120 |
+
|
1121 |
+
reduction_per_guess.append(len(potential_next_guesses))
|
1122 |
+
|
1123 |
+
#### Guessing next word
|
1124 |
+
if len(potential_next_guesses) == 1:
|
1125 |
+
|
1126 |
+
if return_stats == False:
|
1127 |
+
if verbose == True:
|
1128 |
+
st.write(f"All potential next guesses:\n\t{get_word_rating(words_to_rate = list(potential_next_guesses), word_list = word_list)}\n")
|
1129 |
+
st.write(f"Words guessed so far:\n\t{guessed_words}.\n")
|
1130 |
+
|
1131 |
+
st.write(f"The only remaining possible word is:\n\t'{list(potential_next_guesses)[0]}'")
|
1132 |
+
|
1133 |
+
# guess = list(potential_next_guesses)[0]
|
1134 |
+
if guess_num < len(guesses):
|
1135 |
+
guess = guesses[guess_num]
|
1136 |
+
guess_entropies.append(get_word_rating([guess], word_list, normalized = False, ascending = False)[0][1])
|
1137 |
+
|
1138 |
+
else:
|
1139 |
+
|
1140 |
+
best_next_guesses = list(potential_next_guesses)
|
1141 |
+
# # st.write (best_next_guesses)
|
1142 |
+
word_ratings = get_word_rating(best_next_guesses, word_list, normalized = False, ascending = False) # "internal" ratings
|
1143 |
+
|
1144 |
+
# Get max rating of all words
|
1145 |
+
max_rating = -np.inf
|
1146 |
+
for word, rating in word_ratings:
|
1147 |
+
if rating > max_rating:
|
1148 |
+
max_rating = rating
|
1149 |
+
|
1150 |
+
# add best rated words (all equally best rating in next guess list) to set
|
1151 |
+
best_of_the_best_1 = []
|
1152 |
+
for word, rating in word_ratings:
|
1153 |
+
if rating == max_rating:
|
1154 |
+
best_of_the_best_1.append(word)
|
1155 |
+
|
1156 |
+
# only using top ten most frequent prefixes suffixes to bias. After that it the impact is especially negligible
|
1157 |
+
test_starts = get_gram_freq(word_list = word_list, letters_length = 1, position = "start", search = None)[:10]
|
1158 |
+
test_ends = get_gram_freq(word_list = word_list, letters_length = 1, position = "end", search = None)[:10]
|
1159 |
+
|
1160 |
+
# list of the best words that also have the most frequent starting and ending letters (suffixes and prefixes didn't have an impact)
|
1161 |
+
best_of_the_best_2 = []
|
1162 |
+
for start_gram, start_count in test_starts:
|
1163 |
+
for end_gram, end_count in test_ends:
|
1164 |
+
for word in best_of_the_best_1:
|
1165 |
+
if word[:1] == start_gram and word[-1:] == end_gram:
|
1166 |
+
best_of_the_best_2.append(word)
|
1167 |
+
|
1168 |
+
# if len(best_of_the_best_2) > 0:
|
1169 |
+
# guess = best_of_the_best_2[0]
|
1170 |
+
# else:
|
1171 |
+
# guess = best_of_the_best_1[0] # they're all equally the best of the best possible guesses so just pick the first
|
1172 |
+
|
1173 |
+
if guess_num < len(guesses):
|
1174 |
+
guess = guesses[guess_num]
|
1175 |
+
|
1176 |
+
# guess_entropies.append(get_word_rating([guess], word_list, normalized = False, ascending = False)[0][1])
|
1177 |
+
|
1178 |
+
if return_stats == False:
|
1179 |
+
if verbose == True:
|
1180 |
+
if len(word_ratings) <= 40:
|
1181 |
+
st.write(f"All potential next guesses:\n\t{word_ratings}\n")
|
1182 |
+
st.write(f"Words guessed so far:\n\t{guessed_words}.\n")
|
1183 |
+
else:
|
1184 |
+
st.write(f"The top 40 potential next guesses are:\n\t{word_ratings[:40]}\n")
|
1185 |
+
st.write(f"Words guessed so far:\n\t{guessed_words}.\n")
|
1186 |
+
|
1187 |
+
guess_entropies.append(get_word_rating([guess], word_list, normalized = False, ascending = False)[0][1])
|
1188 |
+
|
1189 |
+
#### Guess has now been made -- what to do next
|
1190 |
+
if guess_num == max_guesses: # if at max guesses allowed
|
1191 |
+
guessed_words.append(guess)
|
1192 |
+
stats_dict['target_guessed'] = False
|
1193 |
+
if return_stats == False:
|
1194 |
+
if verbose == True:
|
1195 |
+
st.write("-----------------------------\n")
|
1196 |
+
st.write(f"\nUnfortunately, the puzzle was not solved in {max_guesses} guesses. Better luck next time!")
|
1197 |
+
st.write(f"The target word was '{target}'.\n")
|
1198 |
+
st.write("-----------------------------\n")
|
1199 |
+
else:
|
1200 |
+
st.write(f"\nUnfortunately, the puzzle was not solved in {max_guesses} guesses. Better luck next time!")
|
1201 |
+
st.write(f"The target word was '{target}'.\n")
|
1202 |
+
break
|
1203 |
+
else: # if not at max guesses yet allowed
|
1204 |
+
# stats_dict['target_guessed'] = False
|
1205 |
+
if return_stats == False:
|
1206 |
+
if verbose == True:
|
1207 |
+
if len(potential_next_guesses) > 1:
|
1208 |
+
st.write(f"Recommended next guess:\n\t'{word_ratings[0][0]}'")
|
1209 |
+
|
1210 |
+
# st.write(f"Next guess:\n\t'{guess}'")
|
1211 |
+
st.write("\n-----------------------------\n")
|
1212 |
+
|
1213 |
+
if guess == target:
|
1214 |
+
guess_num += 1
|
1215 |
+
guessed_words.append(guess)
|
1216 |
+
stats_dict['target_guessed'] = True
|
1217 |
+
|
1218 |
+
if return_stats == False:
|
1219 |
+
st.write(f"**Guess {guess_num}: '{guess}'**\n")
|
1220 |
+
st.write(f"You solved the puzzle in {guess_num} guesses!")
|
1221 |
+
|
1222 |
+
if max_guesses - guess_num == 1:
|
1223 |
+
st.write(f"There was only {max_guesses - guess_num} guess remaining.")
|
1224 |
+
else:
|
1225 |
+
st.write(f"There were still {max_guesses - guess_num} guesses remaining.")
|
1226 |
+
|
1227 |
+
if return_stats == False:
|
1228 |
+
# stats_dict['target_guessed'] = True
|
1229 |
+
st.write(f"\nThe target word was **'{target}'**.")
|
1230 |
+
st.write("\n-----------------------------")
|
1231 |
+
break
|
1232 |
+
|
1233 |
+
#### STATS STUFF
|
1234 |
+
mid_guesses_vows = 0
|
1235 |
+
mid_guesses_cons = 0
|
1236 |
+
avg_perf_letters = 0
|
1237 |
+
avg_wrong_pos_letters = 0
|
1238 |
+
avg_wrong_letters = 0
|
1239 |
+
|
1240 |
+
for i, word in enumerate(guessed_words):
|
1241 |
+
mid_guesses_vows += count_vows_cons(word, y_vow = True)['vows']
|
1242 |
+
mid_guesses_cons += count_vows_cons(word, y_vow = True)['cons']
|
1243 |
+
|
1244 |
+
for i in range(0, len(guessed_words) - 1):
|
1245 |
+
avg_perf_letters += perfect_letts_per_guess[i]
|
1246 |
+
avg_wrong_pos_letters += wrong_pos_per_guess[i]
|
1247 |
+
avg_wrong_letters += wrong_letts_per_guess[i]
|
1248 |
+
|
1249 |
+
stats_dict['mid_guesses_avg_vows'] = float(round(mid_guesses_vows / len(guessed_words), 2))
|
1250 |
+
stats_dict['mid_guesses_avg_cons'] = float(round(mid_guesses_cons / len(guessed_words), 2))
|
1251 |
+
|
1252 |
+
stats_dict['avg_perf_letters'] = float(round(np.mean(avg_perf_letters), 2))
|
1253 |
+
stats_dict['avg_wrong_pos_letters'] = float(round(np.mean(avg_wrong_pos_letters), 2))
|
1254 |
+
stats_dict['avg_wrong_letters'] = float(round(np.mean(avg_wrong_letters), 2))
|
1255 |
+
|
1256 |
+
# average number of words remaining after each guess -- the higher this is, the luckier the person got (the lower, the more guesses it took)
|
1257 |
+
stats_dict['avg_remaining'] = float(round(np.mean(reduction_per_guess), 2))
|
1258 |
+
|
1259 |
+
# avg rating of each guessed word relative to all other words possible at that moment -- this should consistently be 100 for the algorithm, but will be different for user
|
1260 |
+
if len(guess_entropies) > 1: # in case of guessing it correctly on the first try
|
1261 |
+
sum_entropies = 0
|
1262 |
+
for rating in guess_entropies:
|
1263 |
+
sum_entropies += rating
|
1264 |
+
|
1265 |
+
average_rating = float(round(sum_entropies / len(guess_entropies), 2))
|
1266 |
+
stats_dict['avg_intermediate_guess_rating'] = average_rating
|
1267 |
+
else:
|
1268 |
+
stats_dict['avg_intermediate_guess_rating'] = float(100)
|
1269 |
+
|
1270 |
+
expected_guesses = 3.85
|
1271 |
+
|
1272 |
+
# guess_num = 3
|
1273 |
+
# average_rating = 95
|
1274 |
+
luck = round(1 - ((((guess_num / expected_guesses) * (stats_dict['avg_intermediate_guess_rating'] / 100)) / max_guesses) * 5), 2)
|
1275 |
+
stats_dict['luck'] = luck
|
1276 |
+
|
1277 |
+
if record == True:
|
1278 |
+
if verbose == True:
|
1279 |
+
with open(f"solutions/{guessed_words[0]}_{target}_wizard_detailed.txt", "w") as fout:
|
1280 |
+
|
1281 |
+
fout.write(line + "\n") # write each line of list of # st.writeed text to .txt file
|
1282 |
+
else:
|
1283 |
+
with open(f"solutions/{guessed_words[0]}_{target}_wizard_summary.txt", "w") as fout:
|
1284 |
+
|
1285 |
+
fout.write(line + "\n") # write
|
1286 |
+
|
1287 |
+
if guess_num <= 6:
|
1288 |
+
stats_dict['valid_success'] = True
|
1289 |
+
else:
|
1290 |
+
stats_dict['valid_success'] = False
|
1291 |
+
|
1292 |
+
stats_dict['num_guesses'] = float(guess_num)
|
1293 |
+
|
1294 |
+
############################################################################################################################################################
|
1295 |
+
############################################################################################################################################################
|
1296 |
+
############################################################################################################################################################
|
1297 |
+
############################################################################################################################################################
|
1298 |
+
|
1299 |
+
def get_gram_freq(word_list: list, letters_length: int = 2, position: bool = "start", search: any = None):
|
1300 |
+
"""
|
1301 |
+
Given a word list, a selected number of letter, a selected word position to start from ("start" or "end"),
|
1302 |
+
and an optional gram to search within the list, this function will get a frequency distribution of all n-grams
|
1303 |
+
from the passed word list and returned a frequency distribution in descending order.
|
1304 |
+
|
1305 |
+
Parameters:
|
1306 |
+
------
|
1307 |
+
`word_list`: list
|
1308 |
+
list of words of the same
|
1309 |
+
`letters_length`: int
|
1310 |
+
number of letters in succession. Size/length of "gram". Must be between 1 and length of words in word list
|
1311 |
+
`position`: bool
|
1312 |
+
Whether to start the gram from the start of the word (like a prefix) or the end of the word (like a suffix)
|
1313 |
+
`search`: str
|
1314 |
+
If != None, string of characters to search for within the generated list. If string not found in list, function will # print an error message.
|
1315 |
+
|
1316 |
+
Returns:
|
1317 |
+
------
|
1318 |
+
`tup`: tuple
|
1319 |
+
If search != None, will return a tuple with the passed search criteria, and its count
|
1320 |
+
`sorted_gram_list`: list
|
1321 |
+
List of tuples in the form of (gram, count) for each combination of the gram size in the pass word_list
|
1322 |
+
"""
|
1323 |
+
|
1324 |
+
gram_freq_dist = {}
|
1325 |
+
|
1326 |
+
for word in word_list:
|
1327 |
+
if position == "start":
|
1328 |
+
gram = word[:letters_length] # first 2 letters
|
1329 |
+
if position == "end":
|
1330 |
+
gram = word[-(letters_length):] # first 2 letters
|
1331 |
+
|
1332 |
+
if gram not in gram_freq_dist:
|
1333 |
+
gram_freq_dist[gram] = 1
|
1334 |
+
else:
|
1335 |
+
gram_freq_dist[gram] += 1
|
1336 |
+
|
1337 |
+
sorted_gram_dist = sorted(gram_freq_dist.items(), key = operator.itemgetter(1), reverse = True)
|
1338 |
+
|
1339 |
+
if search:
|
1340 |
+
nos = []
|
1341 |
+
for tup in sorted_gram_dist:
|
1342 |
+
if tup[0] == search:
|
1343 |
+
return tup
|
1344 |
+
else:
|
1345 |
+
nos.append("not here")
|
1346 |
+
|
1347 |
+
if len(nos) == len(sorted_gram_dist):
|
1348 |
+
print ("Search criteria not found in list. Please enter a gram from within the list.")
|
1349 |
+
else:
|
1350 |
+
return sorted_gram_dist
|
plots.py
ADDED
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from wordle_functions import *
|
2 |
+
import pandas as pd
|
3 |
+
import streamlit as st
|
4 |
+
import plotly.express as px
|
5 |
+
import operator
|
6 |
+
|
7 |
+
### Official wordlist
|
8 |
+
official_words = []
|
9 |
+
with open("data/official_words_processed.txt", "r", encoding = "utf-8") as f:
|
10 |
+
for word in f.read().split("\n"):
|
11 |
+
if word.isalpha():
|
12 |
+
official_words.append(word)
|
13 |
+
f.close() # closes connection to file
|
14 |
+
|
15 |
+
english_alphabet = "abcdefghijklmnopqrstuvwxyz"
|
16 |
+
|
17 |
+
def count_plot():
|
18 |
+
letter_counts = get_letter_counts(word_list = official_words, letters = english_alphabet, sort = "descending", unique = True)
|
19 |
+
letter_counts_dict = {} # {letter : count}
|
20 |
+
letter_counts_dict["Letter"] = []
|
21 |
+
letter_counts_dict["Count"] = []
|
22 |
+
letter_counts_dict["Type"] = []
|
23 |
+
for tup in letter_counts:
|
24 |
+
letter_counts_dict["Letter"].append(tup[0].upper())
|
25 |
+
letter_counts_dict["Count"].append(tup[1])
|
26 |
+
|
27 |
+
if tup[0] in "aeiouy":
|
28 |
+
letter_counts_dict["Type"].append("Vowel")
|
29 |
+
else:
|
30 |
+
letter_counts_dict["Type"].append("Consonant")
|
31 |
+
|
32 |
+
letters_dist_df = pd.DataFrame(letter_counts_dict)
|
33 |
+
|
34 |
+
counts_plot = px.bar(letters_dist_df, x = "Letter", y = "Count", title = "Distribution of Letters in Official Wordle List",
|
35 |
+
color = "Type", color_discrete_map = {"Vowel": "#6ca965", "Consonant": "#c8b653"})
|
36 |
+
counts_plot.update_layout(xaxis = {'categoryorder' : 'total descending'}, title_font_size = 25, font = dict(size = 17))
|
37 |
+
|
38 |
+
# counts_plot.show()
|
39 |
+
st.plotly_chart(counts_plot, use_container_width = True)
|
40 |
+
|
41 |
+
def words_plot():
|
42 |
+
letter_counts = get_letter_counts(word_list = official_words, letters = english_alphabet, sort = "descending", unique = True)
|
43 |
+
total_letters_sum = sum(count for letter, count in letter_counts)
|
44 |
+
|
45 |
+
word_counts = []
|
46 |
+
|
47 |
+
for word in official_words:
|
48 |
+
|
49 |
+
# get set of all letters in the word (this intentionally doesn't count duplicate letters)
|
50 |
+
word_letters = set()
|
51 |
+
for letter in word:
|
52 |
+
word_letters.add(letter)
|
53 |
+
|
54 |
+
# get the sum of all counts of each letter in the word
|
55 |
+
word_sum = 0
|
56 |
+
for letter in word_letters:
|
57 |
+
word_sum += dict(letter_counts)[letter]
|
58 |
+
|
59 |
+
# finally, add the word and its letter count sum to the list
|
60 |
+
word_counts.append((word, round(word_sum / total_letters_sum * 100, 2)))
|
61 |
+
# word_counts
|
62 |
+
### Best and worst x words
|
63 |
+
words_counts_top_10 = sorted(word_counts, key = operator.itemgetter(1), reverse = True)[:5] # top 10 words
|
64 |
+
words_counts_middle_10 = sorted(word_counts, key = operator.itemgetter(1), reverse = True)[(len(word_counts) // 2) - 10 : (len(word_counts) // 2) - 5] # top 10 words
|
65 |
+
words_counts_bottom_10 = sorted(word_counts, key = operator.itemgetter(1), reverse = False)[:6] # bottom 10 words
|
66 |
+
words_counts_x_dict = {}
|
67 |
+
words_counts_x_dict["Word"] = []
|
68 |
+
words_counts_x_dict["Rating"] = []
|
69 |
+
|
70 |
+
for word, rating in words_counts_top_10:
|
71 |
+
words_counts_x_dict["Word"].append(word)
|
72 |
+
words_counts_x_dict["Rating"].append(rating)
|
73 |
+
for word, rating in words_counts_middle_10:
|
74 |
+
words_counts_x_dict["Word"].append(word)
|
75 |
+
words_counts_x_dict["Rating"].append(rating)
|
76 |
+
for word, rating in words_counts_bottom_10:
|
77 |
+
words_counts_x_dict["Word"].append(word)
|
78 |
+
words_counts_x_dict["Rating"].append(rating)
|
79 |
+
|
80 |
+
words_counts_x_df = pd.DataFrame(words_counts_x_dict)
|
81 |
+
words_counts_x_plot = px.bar(words_counts_x_df, x = "Word", y = "Rating", title = "A Selection of Wordle Words and Their Ratings")
|
82 |
+
words_counts_x_plot.update_layout(xaxis = {'categoryorder' : 'total descending'}, title_font_size = 25, font = dict(size = 17))
|
83 |
+
words_counts_x_plot.update_traces(marker_color = "#6ca965")
|
84 |
+
|
85 |
+
# words_counts_x_plot.show()
|
86 |
+
st.plotly_chart(words_counts_x_plot, use_container_width = True)
|
requirements.txt
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
plotly
|
2 |
+
streamlit-extras
|
3 |
+
beautifulsoup4==4.11.1
|
4 |
+
bs4==4.11.1
|
5 |
+
selenium==4.9.0
|
wordle_assistant_functions.py
ADDED
@@ -0,0 +1,1347 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import numpy as np # for stats
|
2 |
+
import random # for randomly generating target and start words
|
3 |
+
import operator # for sorting letter frequency distribution
|
4 |
+
import time # for #dramaticeffect
|
5 |
+
import pandas as pd
|
6 |
+
import streamlit as st
|
7 |
+
|
8 |
+
english_alphabet = "abcdefghijklmnopqrstuvwxyz"
|
9 |
+
|
10 |
+
def get_letter_counts(word_list: list, letters: str = english_alphabet, sort: str = "descending", unique: bool = True):
|
11 |
+
"""
|
12 |
+
Given a passed str of letters and a list of words, produces a frequency distribution of all letters
|
13 |
+
|
14 |
+
Parameters:
|
15 |
+
------
|
16 |
+
`word_list`: list
|
17 |
+
list of words (str) from which word frequencies will be counted
|
18 |
+
`letters`: str
|
19 |
+
a string of letters to be counted. String must only be desired letters, with no spaces. Default is local variable containing all letters of the English alphabet
|
20 |
+
`sort`: str
|
21 |
+
if either "descending" or "ascending" are passed, returned list of tuples will be sorted accordingly, else returned dictionary will be unsorted
|
22 |
+
`unique`: bool
|
23 |
+
if True, only unique letters in a word are counted. That means that words with more unique letters are rated more highly than any words with duplicate letters
|
24 |
+
|
25 |
+
Returns:
|
26 |
+
------
|
27 |
+
`letters_counts_dict`: dict
|
28 |
+
dictionary of {letter : count} pairs for each letter in passed `letters` sequence
|
29 |
+
`sorted_counts_dicts`: list of tuples
|
30 |
+
list of tuples. Format is ("letter", frequency). Ordered according to `sort` values
|
31 |
+
"""
|
32 |
+
|
33 |
+
words_counts_dict = {}
|
34 |
+
|
35 |
+
if unique == False:
|
36 |
+
for word in word_list: # real dataset
|
37 |
+
word_dict = {}
|
38 |
+
|
39 |
+
for letter in word:
|
40 |
+
if letter in word_dict:
|
41 |
+
word_dict[letter] += 1
|
42 |
+
else:
|
43 |
+
word_dict[letter] = 1
|
44 |
+
words_counts_dict[word] = word_dict
|
45 |
+
else: # if unique == True
|
46 |
+
|
47 |
+
for word in word_list: # real dataset
|
48 |
+
word_dict = {}
|
49 |
+
|
50 |
+
word_letters = set(letter for letter in word)
|
51 |
+
|
52 |
+
for letter in word_letters:
|
53 |
+
if letter in word_dict:
|
54 |
+
word_dict[letter] += 1
|
55 |
+
else:
|
56 |
+
word_dict[letter] = 1
|
57 |
+
words_counts_dict[word] = word_dict
|
58 |
+
|
59 |
+
letters_counts_dict = {}
|
60 |
+
|
61 |
+
for letter in letters:
|
62 |
+
letters_counts_dict[letter] = 0
|
63 |
+
|
64 |
+
for word, count_dict in words_counts_dict.items():
|
65 |
+
# # print (word, count_dict)
|
66 |
+
for letter, count in count_dict.items():
|
67 |
+
letters_counts_dict[letter] += count
|
68 |
+
|
69 |
+
if sort == "ascending":
|
70 |
+
sorted_counts_dict = (sorted(letters_counts_dict.items(), key = operator.itemgetter(1), reverse = False))
|
71 |
+
return sorted_counts_dicts
|
72 |
+
|
73 |
+
if sort == "descending":
|
74 |
+
sorted_counts_dict = sorted(letters_counts_dict.items(), key = operator.itemgetter(1), reverse = True)
|
75 |
+
return sorted_counts_dict
|
76 |
+
else:
|
77 |
+
return letters_counts_dict
|
78 |
+
|
79 |
+
### Best first guesses for a given Wordle list
|
80 |
+
|
81 |
+
def best_guess_words(word_list: list, show_letters: bool = False):
|
82 |
+
"""
|
83 |
+
Given a passed list of English words of a consistent length, calculates the most statistically optimal first guess words, alongside a rating for each word.
|
84 |
+
|
85 |
+
Rating = sum(frequency of each unique letter in that word) / sum (all unique letter frequencies in word_list) * 100, rounded to 2 decimals.
|
86 |
+
|
87 |
+
------
|
88 |
+
Parameters:
|
89 |
+
------
|
90 |
+
`word_list`: list
|
91 |
+
list of words (str) of consistent length
|
92 |
+
`show_letters`: bool
|
93 |
+
if True, also # prints set of most optimal letters to guess
|
94 |
+
|
95 |
+
------
|
96 |
+
Returns:
|
97 |
+
------
|
98 |
+
`word_ratings`: list
|
99 |
+
list of tuples. Format is [(word, rating)], where rating is calculated according to above formula
|
100 |
+
`sorted_counts`: list of tuples
|
101 |
+
list of tuples. Format is ("letter", frequency). Sorted according to `sort` value; ["descending" or "ascending"] if passed
|
102 |
+
"""
|
103 |
+
|
104 |
+
english_alphabet = "abcdefghijklmnopqrstuvwxyz"
|
105 |
+
|
106 |
+
sorted_counts = get_letter_counts(english_alphabet, word_list, sort = "descending")
|
107 |
+
|
108 |
+
max_len_possible = len(word_list[0])
|
109 |
+
|
110 |
+
### Get words with the highest letter diversity
|
111 |
+
while max_len_possible:
|
112 |
+
|
113 |
+
best_letters = set()
|
114 |
+
best_words = []
|
115 |
+
|
116 |
+
for letter, freq in sorted_counts:
|
117 |
+
best_letters.add(letter)
|
118 |
+
if len(best_letters) == max_len_possible:
|
119 |
+
break
|
120 |
+
|
121 |
+
### Get all words that have one of each of the 5 top most frequent letters
|
122 |
+
for word in word_list:
|
123 |
+
word_set = set()
|
124 |
+
|
125 |
+
for letter in word:
|
126 |
+
word_set.add(letter)
|
127 |
+
|
128 |
+
if best_letters.issubset(word_set):
|
129 |
+
best_words.append(word)
|
130 |
+
|
131 |
+
if len(best_words) > 0:
|
132 |
+
break
|
133 |
+
else:
|
134 |
+
max_len_possible -= 1 # only try the top 4 letters, then 3, then 2, ...
|
135 |
+
|
136 |
+
if max_len_possible == 0:
|
137 |
+
break
|
138 |
+
|
139 |
+
all_letters_count = 0
|
140 |
+
for letter, freq in sorted_counts:
|
141 |
+
all_letters_count += freq
|
142 |
+
|
143 |
+
word_ratings = []
|
144 |
+
for word in best_words:
|
145 |
+
ratings_dict = {}
|
146 |
+
for letter in word:
|
147 |
+
for freq_letter, freq in sorted_counts:
|
148 |
+
if letter == freq_letter:
|
149 |
+
ratings_dict[letter] = freq
|
150 |
+
|
151 |
+
total_rating = 0
|
152 |
+
for letter, rating in ratings_dict.items():
|
153 |
+
total_rating += rating
|
154 |
+
|
155 |
+
word_ratings.append((word, round(total_rating / all_letters_count * 100, 2)))
|
156 |
+
|
157 |
+
word_ratings = sorted(word_ratings, key = operator.itemgetter(1), reverse = True)
|
158 |
+
|
159 |
+
if show_letters == True:
|
160 |
+
return word_ratings, sorted_counts
|
161 |
+
else:
|
162 |
+
return word_ratings
|
163 |
+
|
164 |
+
def count_vows_cons(word: str, y_vow = True):
|
165 |
+
"""
|
166 |
+
Given a passed word, calculate the number of non-unique vowels and consonants in the word (duplicates counted more than once).
|
167 |
+
|
168 |
+
------
|
169 |
+
Parameters:
|
170 |
+
------
|
171 |
+
`word`: str
|
172 |
+
a single passed word (str)
|
173 |
+
`y_vow`: bool
|
174 |
+
if True, "y" is considered a vowel. If False, "y" considered a consonant. Default is True
|
175 |
+
|
176 |
+
------
|
177 |
+
Returns:
|
178 |
+
------
|
179 |
+
`counts`: dict
|
180 |
+
dictionary, where format is {letter type : count}
|
181 |
+
"""
|
182 |
+
|
183 |
+
word = word.lower() # for consistency
|
184 |
+
|
185 |
+
if y_vow == True:
|
186 |
+
vows = "aeiouy"
|
187 |
+
cons = "bcdfghjklmnpqrstvwxz"
|
188 |
+
elif y_vow == False:
|
189 |
+
vows = "aeiou"
|
190 |
+
cons = "bcdfghjklmnpqrstvwxyz"
|
191 |
+
|
192 |
+
counts = {}
|
193 |
+
counts["vows"] = 0
|
194 |
+
counts["cons"] = 0
|
195 |
+
for letter in word:
|
196 |
+
if letter in vows:
|
197 |
+
counts["vows"] += 1
|
198 |
+
if letter in cons:
|
199 |
+
counts["cons"] += 1
|
200 |
+
|
201 |
+
return counts
|
202 |
+
|
203 |
+
def get_word_rating(words_to_rate: list, word_list: list, normalized: bool = True, ascending: bool = False):
|
204 |
+
"""
|
205 |
+
Given a word and a word list, calculates rating each word as a measure of its impact to the next possible guesses in Wordle, ordered according to `reverse` parameter.
|
206 |
+
|
207 |
+
------
|
208 |
+
Parameters:
|
209 |
+
------
|
210 |
+
`words_to_rate`: list
|
211 |
+
list of strings to be rated
|
212 |
+
`word_list`: list
|
213 |
+
list of all possible words (str) of consistent length, to which each word in `words_to_rate` will be compared
|
214 |
+
`normalized`: bool
|
215 |
+
if True, normalizes all ratings on a scale of 0-100, with 100 being the rating for the most optimal word, and 0 for the least optimal word
|
216 |
+
`ascending`: bool
|
217 |
+
if True, returns list ordered ascending. If False, returns list in descending order
|
218 |
+
|
219 |
+
------
|
220 |
+
Returns:
|
221 |
+
------
|
222 |
+
`word_ratings`: list
|
223 |
+
list of tuples. Format is [(word, rating)], where rating is calculated according to above formula
|
224 |
+
`sorted_counts`: list of tuples
|
225 |
+
list of tuples. Format is ("letter", frequency). Sorted according to `sort` value; ["descending" or "ascending"] if passed
|
226 |
+
"""
|
227 |
+
|
228 |
+
if ascending == True:
|
229 |
+
# sorted_counts = get_letter_counts(english_alphabet, word_list, sort = "ascending")
|
230 |
+
sorted_counts = get_letter_counts(word_list = word_list, letters = english_alphabet, sort = "ascending", unique = True)
|
231 |
+
else:
|
232 |
+
# sorted_counts = get_letter_counts(english_alphabet, word_list, sort = "descending")
|
233 |
+
sorted_counts = get_letter_counts(word_list = word_list, letters = english_alphabet, sort = "descending", unique = True)
|
234 |
+
|
235 |
+
all_letters_count = 0
|
236 |
+
for letter, freq in sorted_counts:
|
237 |
+
all_letters_count += freq
|
238 |
+
|
239 |
+
unnormalized_ratings = []
|
240 |
+
for word in words_to_rate:
|
241 |
+
word = word.lower()
|
242 |
+
ratings_dict = {}
|
243 |
+
for letter in word:
|
244 |
+
for freq_letter, freq in sorted_counts:
|
245 |
+
if letter == freq_letter:
|
246 |
+
ratings_dict[letter] = freq
|
247 |
+
|
248 |
+
total_rating = 0
|
249 |
+
for letter, rating in ratings_dict.items():
|
250 |
+
total_rating += rating
|
251 |
+
|
252 |
+
unnormalized_ratings.append((word, round(total_rating / all_letters_count * 100, 2)))
|
253 |
+
|
254 |
+
word_ratings = sorted(unnormalized_ratings, key = operator.itemgetter(1), reverse = True)
|
255 |
+
# # print (word_ratings)
|
256 |
+
|
257 |
+
if normalized == True:
|
258 |
+
if len(word_ratings) > 1:
|
259 |
+
new_tests = []
|
260 |
+
|
261 |
+
for tup in word_ratings:
|
262 |
+
try:
|
263 |
+
normd = round(((tup[1] - word_ratings[-1][1]) / (word_ratings[0][1] - word_ratings[-1][1])) * 100, 2)
|
264 |
+
new_tests.append((tup[0], normd))
|
265 |
+
except:
|
266 |
+
ZeroDivisionError
|
267 |
+
new_tests.append((tup[0], 0.0))
|
268 |
+
|
269 |
+
return new_tests
|
270 |
+
else:
|
271 |
+
return [(word_ratings[0][0], float(100))]
|
272 |
+
|
273 |
+
elif normalized == False:
|
274 |
+
|
275 |
+
return word_ratings
|
276 |
+
|
277 |
+
### Gets most common words of all words of the dataset
|
278 |
+
|
279 |
+
def get_word_distribution(word_list: list, sort: str = "descending"):
|
280 |
+
"""
|
281 |
+
Given a passed str of words and a list of words, produces a frequency distribution of all words
|
282 |
+
|
283 |
+
------
|
284 |
+
Parameters:
|
285 |
+
------
|
286 |
+
`word_list`: list
|
287 |
+
list of words (str) from which word frequencies will be counted
|
288 |
+
`sort`: str
|
289 |
+
if either "descending" or "ascending" are passed, returned list of tuples will be sorted accoringly, else returned dictionary will be unsorted
|
290 |
+
|
291 |
+
------
|
292 |
+
Returns:
|
293 |
+
------
|
294 |
+
`words_counts_dict`: dict
|
295 |
+
dictionary of {word : count} pairs for each word in passed `word_list`
|
296 |
+
`sorted_counts_dicts`: list of tuples
|
297 |
+
list of tuples. Format is ("word", frequency). Ordered according to `sort` values
|
298 |
+
"""
|
299 |
+
|
300 |
+
words_counts_dict = {}
|
301 |
+
|
302 |
+
for word in word_list:
|
303 |
+
if word in words_counts_dict:
|
304 |
+
words_counts_dict[word] += 1
|
305 |
+
else:
|
306 |
+
words_counts_dict[word] = 1
|
307 |
+
|
308 |
+
if sort == "ascending":
|
309 |
+
sorted_counts_dict = (sorted(words_counts_dict.items(), key = operator.itemgetter(1), reverse = False))
|
310 |
+
return sorted_counts_dict
|
311 |
+
|
312 |
+
if sort == "descending":
|
313 |
+
sorted_counts_dict = sorted(words_counts_dict.items(), key = operator.itemgetter(1), reverse = True)
|
314 |
+
return sorted_counts_dict
|
315 |
+
|
316 |
+
############################################################################################################################################################
|
317 |
+
############################################################################################################################################################
|
318 |
+
############################################################################################################################################################
|
319 |
+
############################################################################################################################################################
|
320 |
+
|
321 |
+
## lines 305 - 835
|
322 |
+
|
323 |
+
# def wordle_wizard(word_list: list, max_guesses: int = None,
|
324 |
+
# guess: str = None, target: str = None,
|
325 |
+
# random_guess: bool = False, random_target: bool = False,
|
326 |
+
# verbose: bool = False, drama: float = None,
|
327 |
+
# return_stats: bool = False, record: bool = False):
|
328 |
+
# """
|
329 |
+
# Mimicking the popular web game, this function matches a current word to a target word automatically, in the most statistically optimal way possible.
|
330 |
+
|
331 |
+
# Parameters:
|
332 |
+
# ------
|
333 |
+
# `word_list`: list
|
334 |
+
# list of valid words to be considered
|
335 |
+
# `guess`: str
|
336 |
+
# a string -- must be the same length as `target_word`
|
337 |
+
# `target`: str
|
338 |
+
# a string -- must be the same length as `opening_word`
|
339 |
+
# `max_guesses`: int
|
340 |
+
# the maximum number of attempts allowed to solve the Wordle
|
341 |
+
# `random_guess`: bool
|
342 |
+
# if True, randomly chooses a starting word from all words within `word_list`. If False, passed starting word must be used instead
|
343 |
+
# `random_target`: bool
|
344 |
+
# if True, randomly chooses a target word from all words within `word_list`. If False, passed target word must be used instead
|
345 |
+
# `verbose`: bool
|
346 |
+
# if True, # prints progress and explanation of how function solves the puzzle. If False, # prints only the guessed word at each guess.
|
347 |
+
# `drama`: float or int
|
348 |
+
# if int provided, each guess' output is delayed by that number of seconds, else each output is shown as quickly as possible. For ~dRaMaTiC eFfEcT~
|
349 |
+
# `return_stats`: bool
|
350 |
+
# if True, # prints nothing and returns a dictionary of various statistics about the function's performance trying to solve the puzzle
|
351 |
+
# `record`: bool
|
352 |
+
# if True, creates a .txt file with the same information # printed according to the indicated verbosity
|
353 |
+
|
354 |
+
# Returns:
|
355 |
+
# ------
|
356 |
+
# `stats_dict`: dict
|
357 |
+
# dictionary containing various statistics about the function's performance trying to solve the puzzle
|
358 |
+
# """
|
359 |
+
|
360 |
+
# guess = guess.lower()
|
361 |
+
# target = target.lower()
|
362 |
+
|
363 |
+
# sugg_words = []
|
364 |
+
|
365 |
+
# for i in range(0, 20):
|
366 |
+
# ran_int = random.randint(0, len(word_list) - 1)
|
367 |
+
# word = word_list[ran_int]
|
368 |
+
# sugg_words.append(word)
|
369 |
+
|
370 |
+
# if guess not in word_list:
|
371 |
+
# # print ("Guess word not in passed word list.\nOnly words within the given word list are valid.")
|
372 |
+
# # print (f"Here are some examples of valid words from the passed word list.\n\t{sugg_words[:10]}")
|
373 |
+
# return None
|
374 |
+
|
375 |
+
# if target not in word_list:
|
376 |
+
# # print ("Target word not in passed word list.\nOnly words within the given word list are valid.")
|
377 |
+
# # print (f"Here are some examples of valid words from the passed word list.\n\t{sugg_words[-10:]}")
|
378 |
+
# return None
|
379 |
+
|
380 |
+
# if random_guess == True:
|
381 |
+
# randomint_guess = random.randint(0, len(word_list) - 1)
|
382 |
+
# guess = word_list[randomint_guess]
|
383 |
+
|
384 |
+
# if random_target == True:
|
385 |
+
# randomint_target = random.randint(0, len(word_list) - 1)
|
386 |
+
# target = word_list[randomint_target]
|
387 |
+
|
388 |
+
# stats_dict = {}
|
389 |
+
# stats_dict['first_guess'] = guess
|
390 |
+
# stats_dict['target_word'] = target
|
391 |
+
# stats_dict['first_guess_vowels'] = float(count_vows_cons(guess, y_vow = True)['vows'])
|
392 |
+
# stats_dict['first_guess_consonants'] = float(count_vows_cons(guess, y_vow = True)['cons'])
|
393 |
+
# stats_dict['target_vowels'] = float(count_vows_cons(target, y_vow = True)['vows'])
|
394 |
+
# stats_dict['target_consonants'] = float(count_vows_cons(target, y_vow = True)['cons'])
|
395 |
+
|
396 |
+
# # get rating of the first guess word and target word in the entire word_list
|
397 |
+
# for tup in get_word_rating(word_list, word_list, normalized = True):
|
398 |
+
# if tup[0] == guess:
|
399 |
+
# stats_dict['first_guess_rating'] = tup[1]
|
400 |
+
# if tup[0] == target:
|
401 |
+
# stats_dict['target_rating'] = tup[1]
|
402 |
+
|
403 |
+
# guess_entropies = []
|
404 |
+
# guess_entropies.append(stats_dict['first_guess_rating'])
|
405 |
+
|
406 |
+
# # luck_guess_1 = round(1 - ((1 / len(word_list)) * guess_entropies[0] / 100), 2) * 100
|
407 |
+
|
408 |
+
# english_alphabet = "abcdefghijklmnopqrstuvwxyz"
|
409 |
+
|
410 |
+
# # word_list_sorted_counts = get_letter_counts(english_alphabet, word_list, sort = "descending")
|
411 |
+
# word_list_sorted_counts = get_letter_counts(word_list = word_list, letters = english_alphabet, sort = "descending", unique = True)
|
412 |
+
|
413 |
+
|
414 |
+
# wordlen = len(guess)
|
415 |
+
# letter_positions = set(i for i in range(0, wordlen))
|
416 |
+
|
417 |
+
# guess_set = set()
|
418 |
+
# perfect_dict = {}
|
419 |
+
# wrong_pos_dict = {}
|
420 |
+
# wrong_pos_set = set()
|
421 |
+
# dont_guess_again = set()
|
422 |
+
|
423 |
+
# guessed_words = [] # running set of guessed words
|
424 |
+
# guess_num = 0 # baseline for variable
|
425 |
+
# dont_guess_words = set()
|
426 |
+
# incorrect_positions = []
|
427 |
+
# reduction_per_guess = []
|
428 |
+
|
429 |
+
# if max_guesses == None: # if no value is passed, default is len(guess)
|
430 |
+
# max_guesses = wordlen
|
431 |
+
# else: # else it is the value passed
|
432 |
+
# max_guesses = max_guesses
|
433 |
+
|
434 |
+
# perfect_letts_per_guess = []
|
435 |
+
# wrong_pos_per_guess = []
|
436 |
+
# wrong_letts_per_guess = []
|
437 |
+
|
438 |
+
# while guess: # while there is any guess -- there are conditions to break it at the bottom
|
439 |
+
|
440 |
+
# guess_num += 1
|
441 |
+
|
442 |
+
# guessed_words.append(guess)
|
443 |
+
|
444 |
+
# if drama:
|
445 |
+
# time.sleep(drama)
|
446 |
+
|
447 |
+
# # guess_num += 1 # each time the guess is processed
|
448 |
+
# if return_stats == False:
|
449 |
+
# if guess_num == 1:
|
450 |
+
# print("-----------------------------\n")
|
451 |
+
|
452 |
+
# if guess == target:
|
453 |
+
# stats_dict['target_guessed'] = True
|
454 |
+
# if return_stats == False:
|
455 |
+
# if guess_num == 1:
|
456 |
+
# # print(f"Congratulations! The Wordle has been solved in {guess_num} guess, that's amazingly lucky!")
|
457 |
+
# print(f"The starting word and target word are the same. Try entering two different words to see how the puzzle can be solved.")
|
458 |
+
# # print(f"The target word was {target}")
|
459 |
+
|
460 |
+
|
461 |
+
# perfect_letts_per_guess.append(5)
|
462 |
+
# wrong_pos_per_guess.append(0)
|
463 |
+
# wrong_letts_per_guess.append(0)
|
464 |
+
# break
|
465 |
+
|
466 |
+
# if return_stats == False:
|
467 |
+
# print(f"**Guess {guess_num}: '{guess}'**")
|
468 |
+
|
469 |
+
# guess_set = set()
|
470 |
+
# wrong_pos_set = set()
|
471 |
+
|
472 |
+
# #### Step 2 -- ALL PERFECT
|
473 |
+
# for i in letter_positions: # number of letters in each word (current word and target word)
|
474 |
+
# guess_set.add(guess[i])
|
475 |
+
|
476 |
+
# if guess[i] not in perfect_dict:
|
477 |
+
# perfect_dict[guess[i]] = set()
|
478 |
+
# if guess[i] not in wrong_pos_dict:
|
479 |
+
# wrong_pos_dict[guess[i]] = set()
|
480 |
+
|
481 |
+
# ### EVALUATE CURRENT GUESS
|
482 |
+
# if guess[i] == target[i]: # letter == correct and position == correct
|
483 |
+
# perfect_dict[guess[i]].add(i)
|
484 |
+
|
485 |
+
# if (guess[i] != target[i] and guess[i] in target): # letter == correct and position != correct
|
486 |
+
# wrong_pos_dict[guess[i]].add(i)
|
487 |
+
# wrong_pos_set.add(guess[i])
|
488 |
+
|
489 |
+
# if guess[i] not in target: # if letter is not relevant at all
|
490 |
+
# dont_guess_again.add(guess[i])
|
491 |
+
|
492 |
+
# #### Step 3 -- ALL PERFECT
|
493 |
+
# next_letters = set()
|
494 |
+
# for letter, positions in perfect_dict.items():
|
495 |
+
# if len(positions) > 0:
|
496 |
+
# next_letters.add(letter)
|
497 |
+
|
498 |
+
# for letter, positions in wrong_pos_dict.items():
|
499 |
+
# if len(positions) > 0:
|
500 |
+
# next_letters.add(letter)
|
501 |
+
|
502 |
+
# #### List of tuples of correct letter positions in new valid words. Eg: [('e', 2), ('a', 3)]
|
503 |
+
# perfect_letters = []
|
504 |
+
# for letter, positions in perfect_dict.items():
|
505 |
+
# for pos in positions:
|
506 |
+
# if len(positions) > 0:
|
507 |
+
# perfect_letters.append((letter, pos))
|
508 |
+
|
509 |
+
# #### all words that have correct letters in same spots
|
510 |
+
# words_matching_correct_all = []
|
511 |
+
# for word in word_list:
|
512 |
+
# word_set = set()
|
513 |
+
# for letter, pos in perfect_letters:
|
514 |
+
# if pos < len(word):
|
515 |
+
# if word[pos] == letter:
|
516 |
+
# words_matching_correct_all.append(word)
|
517 |
+
|
518 |
+
# #### excluding words with letters in known incorrect positions
|
519 |
+
# for letter, positions in wrong_pos_dict.items():
|
520 |
+
# for pos in positions:
|
521 |
+
# if len(positions) > 0:
|
522 |
+
# if (letter, pos) not in incorrect_positions:
|
523 |
+
# incorrect_positions.append((letter, pos))
|
524 |
+
|
525 |
+
# # sorting lists of tuples just to make them look nice in the # printout
|
526 |
+
# incorrect_positions = sorted(incorrect_positions, key = operator.itemgetter(1), reverse = False)
|
527 |
+
# perfect_letters = sorted(perfect_letters, key = operator.itemgetter(1), reverse = False)
|
528 |
+
|
529 |
+
# #### all words that have correct letters in incorrect spots -- so they can be excluded efficiently
|
530 |
+
|
531 |
+
# # print(incorrect_positions)
|
532 |
+
|
533 |
+
# for word in word_list:
|
534 |
+
# word_set = set()
|
535 |
+
# for letter, pos in incorrect_positions:
|
536 |
+
# if pos < len(word):
|
537 |
+
# if word[pos] == letter:
|
538 |
+
# dont_guess_words.add(word)
|
539 |
+
# for word in word_list:
|
540 |
+
# word_set = set()
|
541 |
+
# for letter, pos in incorrect_positions:
|
542 |
+
# if pos < len(word):
|
543 |
+
# if word[pos] == letter:
|
544 |
+
# dont_guess_words.add(word)
|
545 |
+
|
546 |
+
# for bad_letter in dont_guess_again:
|
547 |
+
# for word in word_list:
|
548 |
+
# if (bad_letter in word and word not in dont_guess_words):
|
549 |
+
# dont_guess_words.add(word)
|
550 |
+
|
551 |
+
# if return_stats == False:
|
552 |
+
# if verbose == True:
|
553 |
+
# print(f"Letters in correct positions:\n\t{perfect_letters}\n")
|
554 |
+
# print(f"Letters in incorrect positions:\n\t{incorrect_positions}\n")
|
555 |
+
# # print (f"Letters to guess again:\n\t{sorted(list(next_letters), reverse = False)}\n")
|
556 |
+
# print(f"Letters to not guess again:\n\t{sorted(list(dont_guess_again), reverse = False)}\n") # works
|
557 |
+
|
558 |
+
# # Returns True
|
559 |
+
# # print(A.issubset(B)) # "if everything in A is in B", returns Bool
|
560 |
+
|
561 |
+
# perfect_letts_per_guess.append(len(perfect_letters))
|
562 |
+
# wrong_pos_per_guess.append(len(incorrect_positions))
|
563 |
+
# wrong_letts_per_guess.append(len(dont_guess_again))
|
564 |
+
|
565 |
+
# potential_next_guesses = set()
|
566 |
+
# middle_set = set()
|
567 |
+
|
568 |
+
# if len(perfect_letters) == 0 and len(incorrect_positions) == 0: # if there are NEITHER perfect letters, NOR incorrect positions, ....
|
569 |
+
# for word in word_list:
|
570 |
+
# if word not in dont_guess_words:
|
571 |
+
# if word not in guessed_words:
|
572 |
+
# potential_next_guesses.add(word)
|
573 |
+
|
574 |
+
# # print(f"GUESS {guess_num} : TEST 1-1")
|
575 |
+
|
576 |
+
# if len(perfect_letters) == 0 and len(incorrect_positions) != 0: # if there are no perfect letters whatsoever, but there ARE incorrect positions ....
|
577 |
+
# for word in word_list:
|
578 |
+
# for incor_letter, incor_pos in incorrect_positions:
|
579 |
+
# if incor_pos < len(word):
|
580 |
+
# if word[incor_pos] != incor_letter:
|
581 |
+
# if word not in dont_guess_words: # just in case
|
582 |
+
# word_set = set()
|
583 |
+
# for letter in word:
|
584 |
+
# word_set.add(letter)
|
585 |
+
|
586 |
+
# if next_letters.issubset(word_set):
|
587 |
+
# if word not in guessed_words:
|
588 |
+
# if len(dont_guess_again) > 0:
|
589 |
+
# for bad_letter in dont_guess_again:
|
590 |
+
# if bad_letter not in word:
|
591 |
+
# # potential_next_guesses.append(word)
|
592 |
+
# potential_next_guesses.add(word)
|
593 |
+
# else:
|
594 |
+
# potential_next_guesses.add(word)
|
595 |
+
|
596 |
+
# # print(f"GUESS {guess_num} : TEST 2-1")
|
597 |
+
|
598 |
+
# else:
|
599 |
+
# for word in word_list:
|
600 |
+
# if word not in dont_guess_words: # just in case
|
601 |
+
# word_set = set()
|
602 |
+
# for letter in word:
|
603 |
+
# word_set.add(letter)
|
604 |
+
# if next_letters.issubset(word_set):
|
605 |
+
# if word not in guessed_words:
|
606 |
+
# # # print ("TEST 3-2")
|
607 |
+
|
608 |
+
# if len(dont_guess_again) > 0:
|
609 |
+
# for bad_letter in dont_guess_again:
|
610 |
+
# if bad_letter not in word:
|
611 |
+
# middle_set.add(word)
|
612 |
+
# else:
|
613 |
+
# middle_set.add(word)
|
614 |
+
# for word in middle_set:
|
615 |
+
# dummy_list = []
|
616 |
+
# for good_lett, good_pos in perfect_letters:
|
617 |
+
# if word[good_pos] == good_lett:
|
618 |
+
# dummy_list.append(1)
|
619 |
+
# if len(dummy_list) == len(perfect_letters):
|
620 |
+
# potential_next_guesses.add(word)
|
621 |
+
# for word in middle_set:
|
622 |
+
# dummy_list = []
|
623 |
+
# for bad_lett, bad_pos in incorrect_positions:
|
624 |
+
# if bad_pos < len(word):
|
625 |
+
# if word[bad_pos] == bad_lett:
|
626 |
+
# dummy_list.append(1)
|
627 |
+
# if len(dummy_list) > 0:
|
628 |
+
# potential_next_guesses.remove(word)
|
629 |
+
|
630 |
+
# # print(f"GUESS {guess_num} : TEST 3-1")
|
631 |
+
|
632 |
+
# if return_stats == False:
|
633 |
+
# if verbose == True:
|
634 |
+
# if len(potential_next_guesses) > 1:
|
635 |
+
# # print(f"At this point:")
|
636 |
+
# print(f"\t{len(word_list) - len(potential_next_guesses)}, {round((len(word_list) - len(potential_next_guesses)) / len(word_list) * 100, 2)}% of total words have been eliminated, and")
|
637 |
+
# print(f"\t{len(potential_next_guesses)}, {round(len(potential_next_guesses) / len(word_list) * 100, 2)}% of total words remain possible.\n")
|
638 |
+
|
639 |
+
# else:
|
640 |
+
# # print(f"At this point:")
|
641 |
+
# print(f"\t{len(word_list) - len(potential_next_guesses)}, {round((len(word_list) - len(potential_next_guesses)) / len(word_list) * 100, 2)}% of total words have been eliminated, and")
|
642 |
+
# print(f"\t{len(potential_next_guesses)}, {round(len(potential_next_guesses) / len(word_list) * 100, 2)}% of total words remain possible.\n")
|
643 |
+
|
644 |
+
# reduction_per_guess.append(len(potential_next_guesses))
|
645 |
+
|
646 |
+
# #### Guessing next word
|
647 |
+
# if len(potential_next_guesses) == 1:
|
648 |
+
|
649 |
+
# if return_stats == False:
|
650 |
+
# if verbose == True:
|
651 |
+
# print(f"All potential next guesses:\n\t{get_word_rating(words_to_rate = list(potential_next_guesses), word_list = word_list)}\n")
|
652 |
+
# print(f"Words guessed so far:\n\t{guessed_words}.\n")
|
653 |
+
|
654 |
+
# print(f"The only remaining possible word is:\n\t'{list(potential_next_guesses)[0]}'\n")
|
655 |
+
|
656 |
+
# guess = list(potential_next_guesses)[0]
|
657 |
+
# guess_entropies.append(get_word_rating([guess], word_list, normalized = False, ascending = False)[0][1])
|
658 |
+
|
659 |
+
# else:
|
660 |
+
|
661 |
+
# best_next_guesses = list(potential_next_guesses)
|
662 |
+
# # # print (best_next_guesses)
|
663 |
+
# word_ratings = get_word_rating(best_next_guesses, word_list, normalized = False, ascending = False) # "internal" ratings
|
664 |
+
|
665 |
+
# # Get max rating of all words
|
666 |
+
# max_rating = -np.inf
|
667 |
+
# for word, rating in word_ratings:
|
668 |
+
# if rating > max_rating:
|
669 |
+
# max_rating = rating
|
670 |
+
|
671 |
+
# # add best rated words (all equally best rating in next guess list) to set
|
672 |
+
# best_of_the_best_1 = []
|
673 |
+
# for word, rating in word_ratings:
|
674 |
+
# if rating == max_rating:
|
675 |
+
# best_of_the_best_1.append(word)
|
676 |
+
|
677 |
+
# # only using top ten most frequent prefixes suffixes to bias. After that it the impact is especially negligible
|
678 |
+
# test_starts = get_gram_freq(word_list = word_list, letters_length = 1, position = "start", search = None)[:10]
|
679 |
+
# test_ends = get_gram_freq(word_list = word_list, letters_length = 1, position = "end", search = None)[:10]
|
680 |
+
|
681 |
+
# # list of the best words that also have the most frequent starting and ending letters (suffixes and prefixes didn't have an impact)
|
682 |
+
# best_of_the_best_2 = []
|
683 |
+
# for start_gram, start_count in test_starts:
|
684 |
+
# for end_gram, end_count in test_ends:
|
685 |
+
# for word in best_of_the_best_1:
|
686 |
+
# if word[:1] == start_gram and word[-1:] == end_gram:
|
687 |
+
# best_of_the_best_2.append(word)
|
688 |
+
|
689 |
+
# if len(best_of_the_best_2) > 0:
|
690 |
+
# guess = best_of_the_best_2[0]
|
691 |
+
# else:
|
692 |
+
# guess = best_of_the_best_1[0] # they're all equally the best of the best possible guesses so just pick the first
|
693 |
+
|
694 |
+
# # guess_entropies.append(get_word_rating([guess], word_list, normalized = False, ascending = False)[0][1])
|
695 |
+
|
696 |
+
# if return_stats == False:
|
697 |
+
# if verbose == True:
|
698 |
+
# if len(word_ratings) <= 40:
|
699 |
+
# print(f"All potential next guesses:\n\t{word_ratings}\n")
|
700 |
+
# print(f"Words guessed so far:\n\t{guessed_words}.\n")
|
701 |
+
# else:
|
702 |
+
# print(f"The top 40 potential next guesses are:\n\t{word_ratings[:40]}\n")
|
703 |
+
# print(f"Words guessed so far:\n\t{guessed_words}.\n")
|
704 |
+
|
705 |
+
# guess_entropies.append(get_word_rating([guess], word_list, normalized = False, ascending = False)[0][1])
|
706 |
+
|
707 |
+
# #### Guess has now been made -- what to do next
|
708 |
+
# if guess_num == max_guesses: # if at max guesses allowed
|
709 |
+
# guessed_words.append(guess)
|
710 |
+
# stats_dict['target_guessed'] = False
|
711 |
+
# if return_stats == False:
|
712 |
+
# if verbose == True:
|
713 |
+
# print("-----------------------------\n")
|
714 |
+
# print(f"Unfortunately, the Wordle could not be solved in {max_guesses} guesses.\n")
|
715 |
+
# print(f"The target word was '{target}'.\n")
|
716 |
+
# print("-----------------------------\n")
|
717 |
+
# else:
|
718 |
+
# print(f"\nUnfortunately, Wordle Wizard couldn't solve the puzzle in {max_guesses} guesses. Could you?")
|
719 |
+
# print(f"The target word was '{target}'.\n")
|
720 |
+
# break
|
721 |
+
# else: # if not at max guesses yet allowed
|
722 |
+
# # stats_dict['target_guessed'] = False
|
723 |
+
# if return_stats == False:
|
724 |
+
# if verbose == True:
|
725 |
+
# print(f"Next guess:\n\t'{guess}'")
|
726 |
+
# print("\n-----------------------------\n")
|
727 |
+
|
728 |
+
# if guess == target:
|
729 |
+
# guess_num += 1
|
730 |
+
# guessed_words.append(guess)
|
731 |
+
# stats_dict['target_guessed'] = True
|
732 |
+
|
733 |
+
# if return_stats == False:
|
734 |
+
# print(f"**Guess {guess_num}: '{guess}'**\n")
|
735 |
+
# print(f"Wordle Wizard has solved the puzzle in {guess_num} guesses!")
|
736 |
+
|
737 |
+
# if max_guesses - guess_num == 1:
|
738 |
+
# print(f"There was only {max_guesses - guess_num} guess remaining.")
|
739 |
+
# else:
|
740 |
+
# print(f"There were still {max_guesses - guess_num} guesses remaining.")
|
741 |
+
|
742 |
+
# if return_stats == False:
|
743 |
+
# # stats_dict['target_guessed'] = True
|
744 |
+
# print(f"\nThe target word was **'{target}'**.")
|
745 |
+
# print("\n-----------------------------")
|
746 |
+
# break
|
747 |
+
|
748 |
+
# #### STATS STUFF
|
749 |
+
# mid_guesses_vows = 0
|
750 |
+
# mid_guesses_cons = 0
|
751 |
+
# avg_perf_letters = 0
|
752 |
+
# avg_wrong_pos_letters = 0
|
753 |
+
# avg_wrong_letters = 0
|
754 |
+
|
755 |
+
# for i, word in enumerate(guessed_words):
|
756 |
+
# mid_guesses_vows += count_vows_cons(word, y_vow = True)['vows']
|
757 |
+
# mid_guesses_cons += count_vows_cons(word, y_vow = True)['cons']
|
758 |
+
|
759 |
+
# for i in range(0, len(guessed_words) - 1):
|
760 |
+
# avg_perf_letters += perfect_letts_per_guess[i]
|
761 |
+
# avg_wrong_pos_letters += wrong_pos_per_guess[i]
|
762 |
+
# avg_wrong_letters += wrong_letts_per_guess[i]
|
763 |
+
|
764 |
+
# stats_dict['mid_guesses_avg_vows'] = float(round(mid_guesses_vows / len(guessed_words), 2))
|
765 |
+
# stats_dict['mid_guesses_avg_cons'] = float(round(mid_guesses_cons / len(guessed_words), 2))
|
766 |
+
|
767 |
+
# stats_dict['avg_perf_letters'] = float(round(np.mean(avg_perf_letters), 2))
|
768 |
+
# stats_dict['avg_wrong_pos_letters'] = float(round(np.mean(avg_wrong_pos_letters), 2))
|
769 |
+
# stats_dict['avg_wrong_letters'] = float(round(np.mean(avg_wrong_letters), 2))
|
770 |
+
|
771 |
+
# # average number of words remaining after each guess -- the higher this is, the luckier the person got (the lower, the more guesses it took)
|
772 |
+
# stats_dict['avg_remaining'] = float(round(np.mean(reduction_per_guess), 2))
|
773 |
+
|
774 |
+
# # avg rating of each guessed word relative to all other words possible at that moment -- this should consistently be 100 for the algorithm, but will be different for user
|
775 |
+
# if len(guess_entropies) > 1: # in case of guessing it correctly on the first try
|
776 |
+
# sum_entropies = 0
|
777 |
+
# for rating in guess_entropies:
|
778 |
+
# sum_entropies += rating
|
779 |
+
|
780 |
+
# average_rating = float(round(sum_entropies / len(guess_entropies), 2))
|
781 |
+
# stats_dict['avg_intermediate_guess_rating'] = average_rating
|
782 |
+
# else:
|
783 |
+
# stats_dict['avg_intermediate_guess_rating'] = float(100)
|
784 |
+
|
785 |
+
# expected_guesses = 3.85
|
786 |
+
|
787 |
+
# # guess_num = 3
|
788 |
+
# # average_rating = 95
|
789 |
+
# luck = round(1 - ((((guess_num / expected_guesses) * (stats_dict['avg_intermediate_guess_rating'] / 100)) / max_guesses) * 5), 2)
|
790 |
+
# stats_dict['luck'] = luck
|
791 |
+
|
792 |
+
# if record == True:
|
793 |
+
# if verbose == True:
|
794 |
+
# with open(f"solutions/{guessed_words[0]}_{target}_wizard_detailed.txt", "w") as fout:
|
795 |
+
|
796 |
+
# fout.write(line + "\n") # write each line of list of # printed text to .txt file
|
797 |
+
# else:
|
798 |
+
# with open(f"solutions/{guessed_words[0]}_{target}_wizard_summary.txt", "w") as fout:
|
799 |
+
|
800 |
+
# fout.write(line + "\n") # write
|
801 |
+
|
802 |
+
# # if guess_num <= len(guess):
|
803 |
+
# if guess_num <= 6:
|
804 |
+
# stats_dict['valid_success'] = True
|
805 |
+
# else:
|
806 |
+
# stats_dict['valid_success'] = False
|
807 |
+
|
808 |
+
# stats_dict['num_guesses'] = float(guess_num)
|
809 |
+
|
810 |
+
# # if return_stats == True:
|
811 |
+
# # return stats_dict
|
812 |
+
def wordle_wizard_cheat(guesses: list, word_list: list, max_guesses: int = None,
|
813 |
+
target: str = None,
|
814 |
+
random_guess: bool = False, random_target: bool = False,
|
815 |
+
verbose: bool = False, drama: float = None,
|
816 |
+
return_stats: bool = False, record: bool = False):
|
817 |
+
"""
|
818 |
+
Mimicking the popular web game, this function matches a current word to a target word automatically, in the most statistically optimal way possible.
|
819 |
+
|
820 |
+
Parameters:
|
821 |
+
------
|
822 |
+
`word_list`: list
|
823 |
+
list of valid words to be considered
|
824 |
+
`guess`: str
|
825 |
+
a string -- must be the same length as `target_word`
|
826 |
+
`target`: str
|
827 |
+
a string -- must be the same length as `opening_word`
|
828 |
+
`max_guesses`: int
|
829 |
+
the maximum number of attempts allowed to solve the Wordle
|
830 |
+
`random_guess`: bool
|
831 |
+
if True, randomly chooses a starting word from all words within `word_list`. If False, passed starting word must be used instead
|
832 |
+
`random_target`: bool
|
833 |
+
if True, randomly chooses a target word from all words within `word_list`. If False, passed target word must be used instead
|
834 |
+
`verbose`: bool
|
835 |
+
if True, # st.writes progress and explanation of how function solves the puzzle. If False, # st.writes only the guessed word at each guess.
|
836 |
+
`drama`: float or int
|
837 |
+
if int provided, each guess' output is delayed by that number of seconds, else each output is shown as quickly as possible. For ~dRaMaTiC eFfEcT~
|
838 |
+
`return_stats`: bool
|
839 |
+
if True, # st.writes nothing and returns a dictionary of various statistics about the function's performance trying to solve the puzzle
|
840 |
+
`record`: bool
|
841 |
+
if True, creates a .txt file with the same information # st.writeed according to the indicated verbosity
|
842 |
+
|
843 |
+
Returns:
|
844 |
+
------
|
845 |
+
`stats_dict`: dict
|
846 |
+
dictionary containing various statistics about the function's performance trying to solve the puzzle
|
847 |
+
"""
|
848 |
+
|
849 |
+
# guess = guess.lower()
|
850 |
+
target = target.lower()
|
851 |
+
|
852 |
+
sugg_words = []
|
853 |
+
|
854 |
+
for i in range(0, 20):
|
855 |
+
ran_int = random.randint(0, len(word_list) - 1)
|
856 |
+
word = word_list[ran_int]
|
857 |
+
sugg_words.append(word)
|
858 |
+
|
859 |
+
guess = guesses[0]
|
860 |
+
|
861 |
+
stats_dict = {}
|
862 |
+
stats_dict['first_guess'] = guess
|
863 |
+
stats_dict['target_word'] = target
|
864 |
+
stats_dict['first_guess_vowels'] = float(count_vows_cons(guess, y_vow = True)['vows'])
|
865 |
+
stats_dict['first_guess_consonants'] = float(count_vows_cons(guess, y_vow = True)['cons'])
|
866 |
+
stats_dict['target_vowels'] = float(count_vows_cons(target, y_vow = True)['vows'])
|
867 |
+
stats_dict['target_consonants'] = float(count_vows_cons(target, y_vow = True)['cons'])
|
868 |
+
|
869 |
+
# get rating of the first guess word and target word in the entire word_list
|
870 |
+
for tup in get_word_rating(word_list, word_list, normalized = True):
|
871 |
+
if tup[0] == guess:
|
872 |
+
stats_dict['first_guess_rating'] = tup[1]
|
873 |
+
if tup[0] == target:
|
874 |
+
stats_dict['target_rating'] = tup[1]
|
875 |
+
|
876 |
+
guess_entropies = []
|
877 |
+
guess_entropies.append(stats_dict['first_guess_rating'])
|
878 |
+
|
879 |
+
# luck_guess_1 = round(1 - ((1 / len(word_list)) * guess_entropies[0] / 100), 2) * 100
|
880 |
+
|
881 |
+
english_alphabet = "abcdefghijklmnopqrstuvwxyz"
|
882 |
+
|
883 |
+
# word_list_sorted_counts = get_letter_counts(english_alphabet, word_list, sort = "descending")
|
884 |
+
word_list_sorted_counts = get_letter_counts(word_list = word_list, letters = english_alphabet, sort = "descending", unique = True)
|
885 |
+
|
886 |
+
wordlen = len(guesses[0])
|
887 |
+
letter_positions = set(i for i in range(0, wordlen))
|
888 |
+
|
889 |
+
guess_set = set()
|
890 |
+
perfect_dict = {}
|
891 |
+
wrong_pos_dict = {}
|
892 |
+
wrong_pos_set = set()
|
893 |
+
dont_guess_again = set()
|
894 |
+
|
895 |
+
guessed_words = [] # running set of guessed words
|
896 |
+
guess_num = 0 # baseline for variable
|
897 |
+
dont_guess_words = set()
|
898 |
+
incorrect_positions = []
|
899 |
+
reduction_per_guess = []
|
900 |
+
|
901 |
+
if max_guesses == None: # if no value is passed, default is len(guess)
|
902 |
+
max_guesses = wordlen
|
903 |
+
else: # else it is the value passed
|
904 |
+
max_guesses = max_guesses
|
905 |
+
|
906 |
+
perfect_letts_per_guess = []
|
907 |
+
wrong_pos_per_guess = []
|
908 |
+
wrong_letts_per_guess = []
|
909 |
+
|
910 |
+
# while guess: # while there is any guess -- there are conditions to break it at the bottom
|
911 |
+
|
912 |
+
for guess_num, guess in enumerate(guesses):
|
913 |
+
|
914 |
+
guess_num += 1
|
915 |
+
|
916 |
+
guessed_words.append(guess)
|
917 |
+
|
918 |
+
if drama:
|
919 |
+
time.sleep(drama)
|
920 |
+
|
921 |
+
# guess_num += 1 # each time the guess is processed
|
922 |
+
if return_stats == False:
|
923 |
+
if guess_num == 1:
|
924 |
+
st.write("-----------------------------\n")
|
925 |
+
|
926 |
+
if guess == target:
|
927 |
+
stats_dict['target_guessed'] = True
|
928 |
+
if return_stats == False:
|
929 |
+
if guess_num == 1:
|
930 |
+
# st.write(f"Congratulations! The Wordle has been solved in {guess_num} guess, that's amazingly lucky!")
|
931 |
+
st.write(f"The starting word and target word are the same. Try entering two different words to see how the puzzle can be solved.")
|
932 |
+
# st.write(f"The target word was {target}")
|
933 |
+
|
934 |
+
|
935 |
+
perfect_letts_per_guess.append(5)
|
936 |
+
wrong_pos_per_guess.append(0)
|
937 |
+
wrong_letts_per_guess.append(0)
|
938 |
+
break
|
939 |
+
|
940 |
+
if return_stats == False:
|
941 |
+
st.write(f"**Guess {guess_num}: '{guess}'**")
|
942 |
+
|
943 |
+
guess_set = set()
|
944 |
+
wrong_pos_set = set()
|
945 |
+
|
946 |
+
#### Step 2 -- ALL PERFECT
|
947 |
+
for i in letter_positions: # number of letters in each word (current word and target word)
|
948 |
+
guess_set.add(guess[i])
|
949 |
+
|
950 |
+
if guess[i] not in perfect_dict:
|
951 |
+
perfect_dict[guess[i]] = set()
|
952 |
+
if guess[i] not in wrong_pos_dict:
|
953 |
+
wrong_pos_dict[guess[i]] = set()
|
954 |
+
|
955 |
+
### EVALUATE CURRENT GUESS
|
956 |
+
if guess[i] == target[i]: # letter == correct and position == correct
|
957 |
+
perfect_dict[guess[i]].add(i)
|
958 |
+
|
959 |
+
if (guess[i] != target[i] and guess[i] in target): # letter == correct and position != correct
|
960 |
+
wrong_pos_dict[guess[i]].add(i)
|
961 |
+
wrong_pos_set.add(guess[i])
|
962 |
+
|
963 |
+
if guess[i] not in target: # if letter is not relevant at all
|
964 |
+
dont_guess_again.add(guess[i])
|
965 |
+
|
966 |
+
#### Step 3 -- ALL PERFECT
|
967 |
+
next_letters = set()
|
968 |
+
for letter, positions in perfect_dict.items():
|
969 |
+
if len(positions) > 0:
|
970 |
+
next_letters.add(letter)
|
971 |
+
|
972 |
+
for letter, positions in wrong_pos_dict.items():
|
973 |
+
if len(positions) > 0:
|
974 |
+
next_letters.add(letter)
|
975 |
+
|
976 |
+
#### List of tuples of correct letter positions in new valid words. Eg: [('e', 2), ('a', 3)]
|
977 |
+
perfect_letters = []
|
978 |
+
for letter, positions in perfect_dict.items():
|
979 |
+
for pos in positions:
|
980 |
+
if len(positions) > 0:
|
981 |
+
perfect_letters.append((letter, pos))
|
982 |
+
|
983 |
+
#### all words that have correct letters in same spots
|
984 |
+
words_matching_correct_all = []
|
985 |
+
for word in word_list:
|
986 |
+
word_set = set()
|
987 |
+
for letter, pos in perfect_letters:
|
988 |
+
if pos < len(word):
|
989 |
+
if word[pos] == letter:
|
990 |
+
words_matching_correct_all.append(word)
|
991 |
+
|
992 |
+
#### excluding words with letters in known incorrect positions
|
993 |
+
for letter, positions in wrong_pos_dict.items():
|
994 |
+
for pos in positions:
|
995 |
+
if len(positions) > 0:
|
996 |
+
if (letter, pos) not in incorrect_positions:
|
997 |
+
incorrect_positions.append((letter, pos))
|
998 |
+
|
999 |
+
# sorting lists of tuples just to make them look nice in the # st.writeout
|
1000 |
+
incorrect_positions = sorted(incorrect_positions, key = operator.itemgetter(1), reverse = False)
|
1001 |
+
perfect_letters = sorted(perfect_letters, key = operator.itemgetter(1), reverse = False)
|
1002 |
+
|
1003 |
+
#### all words that have correct letters in incorrect spots -- so they can be excluded efficiently
|
1004 |
+
|
1005 |
+
# st.write(incorrect_positions)
|
1006 |
+
|
1007 |
+
for word in word_list:
|
1008 |
+
word_set = set()
|
1009 |
+
for letter, pos in incorrect_positions:
|
1010 |
+
if pos < len(word):
|
1011 |
+
if word[pos] == letter:
|
1012 |
+
dont_guess_words.add(word)
|
1013 |
+
for word in word_list:
|
1014 |
+
word_set = set()
|
1015 |
+
for letter, pos in incorrect_positions:
|
1016 |
+
if pos < len(word):
|
1017 |
+
if word[pos] == letter:
|
1018 |
+
dont_guess_words.add(word)
|
1019 |
+
|
1020 |
+
for bad_letter in dont_guess_again:
|
1021 |
+
for word in word_list:
|
1022 |
+
if (bad_letter in word and word not in dont_guess_words):
|
1023 |
+
dont_guess_words.add(word)
|
1024 |
+
|
1025 |
+
if return_stats == False:
|
1026 |
+
if verbose == True:
|
1027 |
+
st.write(f"Letters in correct positions:\n\t{perfect_letters}\n")
|
1028 |
+
st.write(f"Letters in incorrect positions:\n\t{incorrect_positions}\n")
|
1029 |
+
# st.write (f"Letters to guess again:\n\t{sorted(list(next_letters), reverse = False)}\n")
|
1030 |
+
st.write(f"Letters to not guess again:\n\t{sorted(list(dont_guess_again), reverse = False)}\n") # works
|
1031 |
+
|
1032 |
+
# Returns True
|
1033 |
+
# st.write(A.issubset(B)) # "if everything in A is in B", returns Bool
|
1034 |
+
|
1035 |
+
perfect_letts_per_guess.append(len(perfect_letters))
|
1036 |
+
wrong_pos_per_guess.append(len(incorrect_positions))
|
1037 |
+
wrong_letts_per_guess.append(len(dont_guess_again))
|
1038 |
+
|
1039 |
+
potential_next_guesses = set()
|
1040 |
+
middle_set = set()
|
1041 |
+
|
1042 |
+
if len(perfect_letters) == 0 and len(incorrect_positions) == 0: # if there are NEITHER perfect letters, NOR incorrect positions, ....
|
1043 |
+
for word in word_list:
|
1044 |
+
if word not in dont_guess_words:
|
1045 |
+
if word not in guessed_words:
|
1046 |
+
potential_next_guesses.add(word)
|
1047 |
+
|
1048 |
+
# st.write(f"GUESS {guess_num} : TEST 1-1")
|
1049 |
+
|
1050 |
+
if len(perfect_letters) == 0 and len(incorrect_positions) != 0: # if there are no perfect letters whatsoever, but there ARE incorrect positions ....
|
1051 |
+
for word in word_list:
|
1052 |
+
for incor_letter, incor_pos in incorrect_positions:
|
1053 |
+
if incor_pos < len(word):
|
1054 |
+
if word[incor_pos] != incor_letter:
|
1055 |
+
if word not in dont_guess_words: # just in case
|
1056 |
+
word_set = set()
|
1057 |
+
for letter in word:
|
1058 |
+
word_set.add(letter)
|
1059 |
+
|
1060 |
+
if next_letters.issubset(word_set):
|
1061 |
+
if word not in guessed_words:
|
1062 |
+
if len(dont_guess_again) > 0:
|
1063 |
+
for bad_letter in dont_guess_again:
|
1064 |
+
if bad_letter not in word:
|
1065 |
+
# potential_next_guesses.append(word)
|
1066 |
+
potential_next_guesses.add(word)
|
1067 |
+
else:
|
1068 |
+
potential_next_guesses.add(word)
|
1069 |
+
|
1070 |
+
# st.write(f"GUESS {guess_num} : TEST 2-1")
|
1071 |
+
|
1072 |
+
else:
|
1073 |
+
for word in word_list:
|
1074 |
+
if word not in dont_guess_words: # just in case
|
1075 |
+
word_set = set()
|
1076 |
+
for letter in word:
|
1077 |
+
word_set.add(letter)
|
1078 |
+
if next_letters.issubset(word_set):
|
1079 |
+
if word not in guessed_words:
|
1080 |
+
# # st.write ("TEST 3-2")
|
1081 |
+
|
1082 |
+
if len(dont_guess_again) > 0:
|
1083 |
+
for bad_letter in dont_guess_again:
|
1084 |
+
if bad_letter not in word:
|
1085 |
+
middle_set.add(word)
|
1086 |
+
else:
|
1087 |
+
middle_set.add(word)
|
1088 |
+
for word in middle_set:
|
1089 |
+
dummy_list = []
|
1090 |
+
for good_lett, good_pos in perfect_letters:
|
1091 |
+
if word[good_pos] == good_lett:
|
1092 |
+
dummy_list.append(1)
|
1093 |
+
if len(dummy_list) == len(perfect_letters):
|
1094 |
+
potential_next_guesses.add(word)
|
1095 |
+
for word in middle_set:
|
1096 |
+
dummy_list = []
|
1097 |
+
for bad_lett, bad_pos in incorrect_positions:
|
1098 |
+
if bad_pos < len(word):
|
1099 |
+
if word[bad_pos] == bad_lett:
|
1100 |
+
dummy_list.append(1)
|
1101 |
+
if len(dummy_list) > 0:
|
1102 |
+
potential_next_guesses.remove(word)
|
1103 |
+
|
1104 |
+
# st.write(f"GUESS {guess_num} : TEST 3-1")
|
1105 |
+
|
1106 |
+
if return_stats == False:
|
1107 |
+
if verbose == True:
|
1108 |
+
if len(potential_next_guesses) > 1:
|
1109 |
+
# st.write(f"At this point:")
|
1110 |
+
st.write(f"\t{len(word_list) - len(potential_next_guesses)}, {round((len(word_list) - len(potential_next_guesses)) / len(word_list) * 100, 2)}% of total words have been eliminated, and")
|
1111 |
+
st.write(f"\t{len(potential_next_guesses)}, {round(len(potential_next_guesses) / len(word_list) * 100, 2)}% of total words remain possible.\n")
|
1112 |
+
|
1113 |
+
else:
|
1114 |
+
# st.write(f"At this point:")
|
1115 |
+
st.write(f"\t{len(word_list) - len(potential_next_guesses)}, {round((len(word_list) - len(potential_next_guesses)) / len(word_list) * 100, 2)}% of total words have been eliminated, and")
|
1116 |
+
st.write(f"\t{len(potential_next_guesses)}, {round(len(potential_next_guesses) / len(word_list) * 100, 2)}% of total words remain possible.\n")
|
1117 |
+
|
1118 |
+
reduction_per_guess.append(len(potential_next_guesses))
|
1119 |
+
|
1120 |
+
#### Guessing next word
|
1121 |
+
if len(potential_next_guesses) == 1:
|
1122 |
+
|
1123 |
+
if return_stats == False:
|
1124 |
+
if verbose == True:
|
1125 |
+
st.write(f"All potential next guesses:\n\t{get_word_rating(words_to_rate = list(potential_next_guesses), word_list = word_list)}\n")
|
1126 |
+
st.write(f"Words guessed so far:\n\t{guessed_words}.\n")
|
1127 |
+
|
1128 |
+
st.write(f"The only remaining possible word is:\n\t'{list(potential_next_guesses)[0]}'")
|
1129 |
+
|
1130 |
+
# guess = list(potential_next_guesses)[0]
|
1131 |
+
if guess_num < len(guesses):
|
1132 |
+
guess = guesses[guess_num]
|
1133 |
+
guess_entropies.append(get_word_rating([guess], word_list, normalized = False, ascending = False)[0][1])
|
1134 |
+
|
1135 |
+
else:
|
1136 |
+
|
1137 |
+
best_next_guesses = list(potential_next_guesses)
|
1138 |
+
# # st.write (best_next_guesses)
|
1139 |
+
word_ratings = get_word_rating(best_next_guesses, word_list, normalized = False, ascending = False) # "internal" ratings
|
1140 |
+
|
1141 |
+
# Get max rating of all words
|
1142 |
+
max_rating = -np.inf
|
1143 |
+
for word, rating in word_ratings:
|
1144 |
+
if rating > max_rating:
|
1145 |
+
max_rating = rating
|
1146 |
+
|
1147 |
+
# add best rated words (all equally best rating in next guess list) to set
|
1148 |
+
best_of_the_best_1 = []
|
1149 |
+
for word, rating in word_ratings:
|
1150 |
+
if rating == max_rating:
|
1151 |
+
best_of_the_best_1.append(word)
|
1152 |
+
|
1153 |
+
# only using top ten most frequent prefixes suffixes to bias. After that it the impact is especially negligible
|
1154 |
+
test_starts = get_gram_freq(word_list = word_list, letters_length = 1, position = "start", search = None)[:10]
|
1155 |
+
test_ends = get_gram_freq(word_list = word_list, letters_length = 1, position = "end", search = None)[:10]
|
1156 |
+
|
1157 |
+
# list of the best words that also have the most frequent starting and ending letters (suffixes and prefixes didn't have an impact)
|
1158 |
+
best_of_the_best_2 = []
|
1159 |
+
for start_gram, start_count in test_starts:
|
1160 |
+
for end_gram, end_count in test_ends:
|
1161 |
+
for word in best_of_the_best_1:
|
1162 |
+
if word[:1] == start_gram and word[-1:] == end_gram:
|
1163 |
+
best_of_the_best_2.append(word)
|
1164 |
+
|
1165 |
+
# if len(best_of_the_best_2) > 0:
|
1166 |
+
# guess = best_of_the_best_2[0]
|
1167 |
+
# else:
|
1168 |
+
# guess = best_of_the_best_1[0] # they're all equally the best of the best possible guesses so just pick the first
|
1169 |
+
|
1170 |
+
if guess_num < len(guesses):
|
1171 |
+
guess = guesses[guess_num]
|
1172 |
+
|
1173 |
+
# guess_entropies.append(get_word_rating([guess], word_list, normalized = False, ascending = False)[0][1])
|
1174 |
+
|
1175 |
+
if return_stats == False:
|
1176 |
+
if verbose == True:
|
1177 |
+
if len(word_ratings) <= 40:
|
1178 |
+
st.write(f"All potential next guesses:\n\t{word_ratings}\n")
|
1179 |
+
st.write(f"Words guessed so far:\n\t{guessed_words}.\n")
|
1180 |
+
else:
|
1181 |
+
st.write(f"The top 40 potential next guesses are:\n\t{word_ratings[:40]}\n")
|
1182 |
+
st.write(f"Words guessed so far:\n\t{guessed_words}.\n")
|
1183 |
+
|
1184 |
+
guess_entropies.append(get_word_rating([guess], word_list, normalized = False, ascending = False)[0][1])
|
1185 |
+
|
1186 |
+
#### Guess has now been made -- what to do next
|
1187 |
+
if guess_num == max_guesses: # if at max guesses allowed
|
1188 |
+
guessed_words.append(guess)
|
1189 |
+
stats_dict['target_guessed'] = False
|
1190 |
+
if return_stats == False:
|
1191 |
+
if verbose == True:
|
1192 |
+
st.write("-----------------------------\n")
|
1193 |
+
st.write(f"\nUnfortunately, the puzzle was not solved in {max_guesses} guesses. Better luck next time!")
|
1194 |
+
st.write(f"The target word was '{target}'.\n")
|
1195 |
+
st.write("-----------------------------\n")
|
1196 |
+
else:
|
1197 |
+
st.write(f"\nUnfortunately, the puzzle was not solved in {max_guesses} guesses. Better luck next time!")
|
1198 |
+
st.write(f"The target word was '{target}'.\n")
|
1199 |
+
break
|
1200 |
+
else: # if not at max guesses yet allowed
|
1201 |
+
# stats_dict['target_guessed'] = False
|
1202 |
+
if return_stats == False:
|
1203 |
+
if verbose == True:
|
1204 |
+
if len(potential_next_guesses) > 1:
|
1205 |
+
st.write(f"Recommended next guess:\n\t'{word_ratings[0][0]}'")
|
1206 |
+
|
1207 |
+
# st.write(f"Next guess:\n\t'{guess}'")
|
1208 |
+
st.write("\n-----------------------------\n")
|
1209 |
+
|
1210 |
+
if guess == target:
|
1211 |
+
guess_num += 1
|
1212 |
+
guessed_words.append(guess)
|
1213 |
+
stats_dict['target_guessed'] = True
|
1214 |
+
|
1215 |
+
if return_stats == False:
|
1216 |
+
st.write(f"**Guess {guess_num}: '{guess}'**\n")
|
1217 |
+
st.write(f"You solved the puzzle in {guess_num} guesses!")
|
1218 |
+
|
1219 |
+
if max_guesses - guess_num == 1:
|
1220 |
+
st.write(f"There was only {max_guesses - guess_num} guess remaining.")
|
1221 |
+
else:
|
1222 |
+
st.write(f"There were still {max_guesses - guess_num} guesses remaining.")
|
1223 |
+
|
1224 |
+
if return_stats == False:
|
1225 |
+
# stats_dict['target_guessed'] = True
|
1226 |
+
st.write(f"\nThe target word was **'{target}'**.")
|
1227 |
+
st.write("\n-----------------------------")
|
1228 |
+
break
|
1229 |
+
|
1230 |
+
#### STATS STUFF
|
1231 |
+
mid_guesses_vows = 0
|
1232 |
+
mid_guesses_cons = 0
|
1233 |
+
avg_perf_letters = 0
|
1234 |
+
avg_wrong_pos_letters = 0
|
1235 |
+
avg_wrong_letters = 0
|
1236 |
+
|
1237 |
+
for i, word in enumerate(guessed_words):
|
1238 |
+
mid_guesses_vows += count_vows_cons(word, y_vow = True)['vows']
|
1239 |
+
mid_guesses_cons += count_vows_cons(word, y_vow = True)['cons']
|
1240 |
+
|
1241 |
+
for i in range(0, len(guessed_words) - 1):
|
1242 |
+
avg_perf_letters += perfect_letts_per_guess[i]
|
1243 |
+
avg_wrong_pos_letters += wrong_pos_per_guess[i]
|
1244 |
+
avg_wrong_letters += wrong_letts_per_guess[i]
|
1245 |
+
|
1246 |
+
stats_dict['mid_guesses_avg_vows'] = float(round(mid_guesses_vows / len(guessed_words), 2))
|
1247 |
+
stats_dict['mid_guesses_avg_cons'] = float(round(mid_guesses_cons / len(guessed_words), 2))
|
1248 |
+
|
1249 |
+
stats_dict['avg_perf_letters'] = float(round(np.mean(avg_perf_letters), 2))
|
1250 |
+
stats_dict['avg_wrong_pos_letters'] = float(round(np.mean(avg_wrong_pos_letters), 2))
|
1251 |
+
stats_dict['avg_wrong_letters'] = float(round(np.mean(avg_wrong_letters), 2))
|
1252 |
+
|
1253 |
+
# average number of words remaining after each guess -- the higher this is, the luckier the person got (the lower, the more guesses it took)
|
1254 |
+
stats_dict['avg_remaining'] = float(round(np.mean(reduction_per_guess), 2))
|
1255 |
+
|
1256 |
+
# avg rating of each guessed word relative to all other words possible at that moment -- this should consistently be 100 for the algorithm, but will be different for user
|
1257 |
+
if len(guess_entropies) > 1: # in case of guessing it correctly on the first try
|
1258 |
+
sum_entropies = 0
|
1259 |
+
for rating in guess_entropies:
|
1260 |
+
sum_entropies += rating
|
1261 |
+
|
1262 |
+
average_rating = float(round(sum_entropies / len(guess_entropies), 2))
|
1263 |
+
stats_dict['avg_intermediate_guess_rating'] = average_rating
|
1264 |
+
else:
|
1265 |
+
stats_dict['avg_intermediate_guess_rating'] = float(100)
|
1266 |
+
|
1267 |
+
expected_guesses = 3.85
|
1268 |
+
|
1269 |
+
# guess_num = 3
|
1270 |
+
# average_rating = 95
|
1271 |
+
luck = round(1 - ((((guess_num / expected_guesses) * (stats_dict['avg_intermediate_guess_rating'] / 100)) / max_guesses) * 5), 2)
|
1272 |
+
stats_dict['luck'] = luck
|
1273 |
+
|
1274 |
+
if record == True:
|
1275 |
+
if verbose == True:
|
1276 |
+
with open(f"solutions/{guessed_words[0]}_{target}_wizard_detailed.txt", "w") as fout:
|
1277 |
+
|
1278 |
+
fout.write(line + "\n") # write each line of list of # st.writeed text to .txt file
|
1279 |
+
else:
|
1280 |
+
with open(f"solutions/{guessed_words[0]}_{target}_wizard_summary.txt", "w") as fout:
|
1281 |
+
|
1282 |
+
fout.write(line + "\n") # write
|
1283 |
+
|
1284 |
+
if guess_num <= 6:
|
1285 |
+
stats_dict['valid_success'] = True
|
1286 |
+
else:
|
1287 |
+
stats_dict['valid_success'] = False
|
1288 |
+
|
1289 |
+
stats_dict['num_guesses'] = float(guess_num)
|
1290 |
+
|
1291 |
+
############################################################################################################################################################
|
1292 |
+
############################################################################################################################################################
|
1293 |
+
############################################################################################################################################################
|
1294 |
+
############################################################################################################################################################
|
1295 |
+
|
1296 |
+
def get_gram_freq(word_list: list, letters_length: int = 2, position: bool = "start", search: any = None):
|
1297 |
+
"""
|
1298 |
+
Given a word list, a selected number of letter, a selected word position to start from ("start" or "end"),
|
1299 |
+
and an optional gram to search within the list, this function will get a frequency distribution of all n-grams
|
1300 |
+
from the passed word list and returned a frequency distribution in descending order.
|
1301 |
+
|
1302 |
+
Parameters:
|
1303 |
+
------
|
1304 |
+
`word_list`: list
|
1305 |
+
list of words of the same
|
1306 |
+
`letters_length`: int
|
1307 |
+
number of letters in succession. Size/length of "gram". Must be between 1 and length of words in word list
|
1308 |
+
`position`: bool
|
1309 |
+
Whether to start the gram from the start of the word (like a prefix) or the end of the word (like a suffix)
|
1310 |
+
`search`: str
|
1311 |
+
If != None, string of characters to search for within the generated list. If string not found in list, function will # print an error message.
|
1312 |
+
|
1313 |
+
Returns:
|
1314 |
+
------
|
1315 |
+
`tup`: tuple
|
1316 |
+
If search != None, will return a tuple with the passed search criteria, and its count
|
1317 |
+
`sorted_gram_list`: list
|
1318 |
+
List of tuples in the form of (gram, count) for each combination of the gram size in the pass word_list
|
1319 |
+
"""
|
1320 |
+
|
1321 |
+
gram_freq_dist = {}
|
1322 |
+
|
1323 |
+
for word in word_list:
|
1324 |
+
if position == "start":
|
1325 |
+
gram = word[:letters_length] # first 2 letters
|
1326 |
+
if position == "end":
|
1327 |
+
gram = word[-(letters_length):] # first 2 letters
|
1328 |
+
|
1329 |
+
if gram not in gram_freq_dist:
|
1330 |
+
gram_freq_dist[gram] = 1
|
1331 |
+
else:
|
1332 |
+
gram_freq_dist[gram] += 1
|
1333 |
+
|
1334 |
+
sorted_gram_dist = sorted(gram_freq_dist.items(), key = operator.itemgetter(1), reverse = True)
|
1335 |
+
|
1336 |
+
if search:
|
1337 |
+
nos = []
|
1338 |
+
for tup in sorted_gram_dist:
|
1339 |
+
if tup[0] == search:
|
1340 |
+
return tup
|
1341 |
+
else:
|
1342 |
+
nos.append("not here")
|
1343 |
+
|
1344 |
+
if len(nos) == len(sorted_gram_dist):
|
1345 |
+
print ("Search criteria not found in list. Please enter a gram from within the list.")
|
1346 |
+
else:
|
1347 |
+
return sorted_gram_dist
|