KAI MAURIN-JONES commited on
Commit
6a1c250
β€’
1 Parent(s): fea2e56

files added

Browse files
.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
- title: Wordle Wizard Assistant
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
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
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