If you're looking for a complete Python Selenium solution for solving the Wordle Game programmatically, here's one that uses the SeleniumBase framework. The solution comes with a YouTube video, as well as the Python code of the solution, and a GIF of what to expect:
The code uses special SeleniumBase ::shadow
selectors in order to pierce through multiple layers of Shadow-DOM. Here's the code below, which can be run after calling pip install seleniumbase
to get all the Python dependencies: (Updated 2/11/22 - after NYT acquired Wordle; Updated again on 2/13/22 to fix font issue on some Python environments; Updated again on 7/1/22 to use web-archived versions of Wordle because NYT removed the Shadow-DOM elements from the latest Wordle.)
"""
Solve the Wordle game using SeleniumBase.
This test runs on archived versions of Wordle, containing Shadow-DOM.
"""
import ast
import random
import requests
from seleniumbase import version_tuple
from seleniumbase import BaseCase
class WordleTests(BaseCase):
word_list = []
def initialize_word_list(self):
txt_file = "https://seleniumbase.github.io/cdn/txt/wordle_words.txt"
word_string = requests.get(txt_file).text
self.word_list = ast.literal_eval(word_string)
def modify_word_list(self, word, letter_status):
new_word_list = []
correct_letters = []
present_letters = []
for i in range(len(word)):
if letter_status[i] == "correct":
correct_letters.append(word[i])
for w in self.word_list:
if w[i] == word[i]:
new_word_list.append(w)
self.word_list = new_word_list
new_word_list = []
for i in range(len(word)):
if letter_status[i] == "present":
present_letters.append(word[i])
for w in self.word_list:
if word[i] in w and word[i] != w[i]:
new_word_list.append(w)
self.word_list = new_word_list
new_word_list = []
for i in range(len(word)):
if letter_status[i] == "absent":
if (
word[i] not in correct_letters
and word[i] not in present_letters
):
for w in self.word_list:
if word[i] not in w:
new_word_list.append(w)
else:
for w in self.word_list:
if word[i] != w[i]:
new_word_list.append(w)
self.word_list = new_word_list
new_word_list = []
def skip_if_incorrect_env(self):
if self.headless:
message = "This test doesn't run in headless mode!"
print(message)
self.skip(message)
if not self.is_chromium():
message = "This test requires a Chromium-based browser!"
print(message)
self.skip(message)
if version_tuple < (4, 0, 0):
message = "This test requires SeleniumBase 4.0.0 or newer!"
print(message)
self.skip(message)
def test_wordle(self):
self.skip_if_incorrect_env()
random.seed()
year = "2022"
month = random.randint(3, 5)
day = random.randint(1, 30)
date = str(year) + "0" + str(month) + str(day)
archive = "https://web.archive.org/web/"
url = "https://www.nytimes.com/games/wordle/index.html"
past_wordle = archive + date + "/" + url
print("\n" + past_wordle)
self.open(past_wordle)
self.wait_for_element("#wm-ipp-base")
self.remove_elements("#wm-ipp-base")
self.click("game-app::shadow game-modal::shadow game-icon")
self.initialize_word_list()
keyboard_base = "game-app::shadow game-keyboard::shadow "
word = random.choice(self.word_list)
num_attempts = 0
found_word = False
for attempt in range(6):
num_attempts += 1
word = random.choice(self.word_list)
letters = []
for letter in word:
letters.append(letter)
button = 'button[data-key="%s"]' % letter
self.click(keyboard_base + button)
button = "button.one-and-a-half"
self.click(keyboard_base + button)
row = 'game-app::shadow game-row[letters="%s"]::shadow ' % word
tile = row + "game-tile:nth-of-type(%s)"
self.wait_for_element(tile % "5" + '::shadow [data-state$="t"]')
self.wait_for_element(
tile % "5" + '::shadow [data-animation="idle"]'
)
letter_status = []
for i in range(1, 6):
letter_eval = self.get_attribute(tile % str(i), "evaluation")
letter_status.append(letter_eval)
if letter_status.count("correct") == 5:
found_word = True
break
self.word_list.remove(word)
self.modify_word_list(word, letter_status)
self.save_screenshot_to_logs()
if found_word:
print('Word: "%s"\nAttempts: %s' % (word.upper(), num_attempts))
else:
print('Final guess: "%s" (Not the correct word!)' % word.upper())
self.fail("Unable to solve for the correct word in 6 attempts!")
self.sleep(3)
Note that SeleniumBase tests are run using pytest
. Also, the Wordle website appears slightly differently when opened using headless Chrome, so don't use Chrome's headless mode when running this example.
Have fun solving "Wordle" with SeleniumBase using Python and Selenium!
Here's a script that works on the latest version of Wordle, which removed the Shadow-DOM elements:
"""Solve Wordle with SeleniumBase."""
import ast
import random
import requests
from seleniumbase import BaseCase
class WordleTests(BaseCase):
word_list = []
def initialize_word_list(self):
txt_file = "https://seleniumbase.github.io/cdn/txt/wordle_words.txt"
word_string = requests.get(txt_file).text
self.word_list = ast.literal_eval(word_string)
def modify_word_list(self, word, letter_status):
new_word_list = []
correct_letters = []
present_letters = []
for i in range(len(word)):
if letter_status[i] == "correct":
correct_letters.append(word[i])
for w in self.word_list:
if w[i] == word[i]:
new_word_list.append(w)
self.word_list = new_word_list
new_word_list = []
for i in range(len(word)):
if letter_status[i] == "present":
present_letters.append(word[i])
for w in self.word_list:
if word[i] in w and word[i] != w[i]:
new_word_list.append(w)
self.word_list = new_word_list
new_word_list = []
for i in range(len(word)):
if letter_status[i] == "absent":
if (
word[i] not in correct_letters
and word[i] not in present_letters
):
for w in self.word_list:
if word[i] not in w:
new_word_list.append(w)
else:
for w in self.word_list:
if word[i] != w[i]:
new_word_list.append(w)
self.word_list = new_word_list
new_word_list = []
def test_wordle(self):
self.open("https://www.nytimes.com/games/wordle/index.html")
self.click_if_visible('svg[data-testid="icon-close"]', timeout=2)
self.remove_elements("div.place-ad")
self.initialize_word_list()
random.seed()
word = random.choice(self.word_list)
num_attempts = 0
found_word = False
for attempt in range(6):
num_attempts += 1
word = random.choice(self.word_list)
letters = []
for letter in word:
letters.append(letter)
button = 'button[data-key="%s"]' % letter
self.click(button)
button = 'button[class*="oneAndAHalf"]'
self.click(button)
row = (
'div[class*="Board"] div[class*="Row-module"]:nth-of-type(%s) '
% num_attempts
)
tile = row + 'div:nth-child(%s) div[class*="module_tile__"]'
self.wait_for_element(tile % "5" + '[data-state$="t"]')
self.wait_for_element(tile % "5" + '[data-animation="idle"]')
letter_status = []
for i in range(1, 6):
letter_eval = self.get_attribute(tile % str(i), "data-state")
letter_status.append(letter_eval)
if letter_status.count("correct") == 5:
found_word = True
break
self.word_list.remove(word)
self.modify_word_list(word, letter_status)
self.save_screenshot_to_logs()
if found_word:
print('\nWord: "%s"\nAttempts: %s' % (word.upper(), num_attempts))
else:
print('Final guess: "%s" (Not the correct word!)' % word.upper())
self.fail("Unable to solve for the correct word in 6 attempts!")
self.sleep(3)
Top comments (23)
I have a question which is less about your script than it is about using pytest. I ran pytest -s wordle.py and it runs fine and prints the result to the console. However, I wanted to run the script while logged in via an ssh session and in that case Chrome opens on the Mac screen but does not then run the script. Is there a way to run this "remotely"?
For running Selenium tests remotely, there is something called a "Selenium Grid". Some info on that here: github.com/seleniumbase/SeleniumBa...
Selenium Grid is the right way to do remote execution but I found a less attractive way which is to start a terminal session with the screen command and detach the console. If I then ssh in and reattach the console, I am able to run the pytest and get the print output back.
I meant to say that my question is probably more about selenium and how to run this script remotely.
New problem. I have this working perfectly on an iMac (with Chrome) but after installing seleniumbase and pytest on my MBP (which is running the same MacOS and Version of Chrome), I am getting this error:
/anaconda3/lib/python3.7/site-packages/seleniumbase/fixtures/page_actions.py:163: ElementNotVisibleException
Any idea of what is causing this?
The New York Times just acquired Wordle, and there were some strange things happening during the transition for a short time. If there are any issues remaining, I'll be posting an updated script soon.
I will be very interested to see if the transition is the cause of this problem. Your script continues to work fine on the iMac but not on the MacBook Pro regardless of which browser is used.
It could be related to the Python version. I see you are using
3.7
. Try a newer one (3.8
or3.9
or3.10
). I also updated the script to point directly to the newer NYT URL, and to get the words list must faster without any redirects.The current script on github no longer works on my iMac or MBP (under either python 3.7 or 3.9).
What's the error message? I was able to run it now just fine.
I stand corrected. The script works on my iMac under python 3.9 but no longer runs under 3.7. However, on the MBP I also have python 3.9 env activated (conda) but when I run pytest it still seems to be using python 3.7 (not sure how to change that) to see if the new script runs.
You could try
python3.9 -m pytest
in order to force pytest to use a specific version of Python if that version is installed.On the MBP running with 3.7 the browser window comes up and the 1st word is entered and then it times out. WIth 3.9 the console log for the MBP is here: gist.github.com/ifuchs/e3ee59aec78...
Obviously there is something wrong with the environment on the MBP so it is not your code that is the problem.
Looks to be a missing font issue:
'button[data-key="↵"]'
shows up as
'button[data-key="?"]'
If you change
button[data-key="↵"]
to
button.one-and-a-half
does it start working?
YES! Not only did that fix it under 3.9, it now works properly under 3.7 as well (same as on the iMac). Thanks for your help. (So, how to fix the font "issue" on the MBP?)
and what is button.one-and-a-half?
Not sure about getting the missing fonts on your MBP's Python env, but I've already updated the script on the DEV post to use a selector without the special font, and I'll be shipping that change to the SeleniumBase repo on GitHub within the hour. Thanks for finding that font issue on some Python environments!
button.one-and-a-half
is another selector for the Enter button. It has that class name because the button is wider than other buttons on that keyboard.Thanks. I searched but did not find it in the seleniumbase docs.
Is there a way to run the pytest repetitively so that it shows each path to solution, waits a few seconds and does it again?
One: That
button.one-and-a-half
selector is part of the Wordle website:Two: Here's an example SeleniumBase test for repeating tests: test_repeat_tests.py
All necessary updates to the SeleniumBase GitHub repo have been shipped.
I tried installing pytest-repeat and running
pytest --count=5 wordle_test.py but that only ran it once.
I'll see if I can use your examples do cause it to repeat.
pytest-repeat
is an external plugin that won't work on tests that inheritunittest.TestCase
.