|
| 1 | +# https://www.wordunscrambler.net/word-list/wordle-word-list |
| 2 | +# for the list of words |
| 3 | + |
| 4 | +from selenium import webdriver |
| 5 | +from selenium.webdriver.common.by import By |
| 6 | +from selenium.webdriver.firefox.service import Service as FirefoxService |
| 7 | +from webdriver_manager.firefox import GeckoDriverManager |
| 8 | +from pynput import keyboard |
| 9 | +import time |
| 10 | + |
| 11 | + |
| 12 | +# This class is used to store data about the wordle such as : |
| 13 | +# - the list of possible words |
| 14 | +# - the letters that are present but not in the right position |
| 15 | +# - the letters that are absent |
| 16 | +# - the letters that are correct and their position in a list |
| 17 | +# - the word that is currently being tested |
| 18 | + |
| 19 | +class Finder: |
| 20 | + def __init__(self): |
| 21 | + self.possible_words = get_list_of_words() |
| 22 | + self.present_letters = set([]) |
| 23 | + self.absent_letters = set([]) |
| 24 | + self.word = [''] * 5 |
| 25 | + |
| 26 | + # Creators recommend “Slate” as starting word |
| 27 | + self.word_to_try = "slate" |
| 28 | + |
| 29 | + |
| 30 | +# Function that is called by the KeyboardListener |
| 31 | +def on_release(key): |
| 32 | + # Start button |
| 33 | + if key == keyboard.Key.esc: |
| 34 | + return False # stop listener |
| 35 | + |
| 36 | + |
| 37 | +# Get the status of the letters in the wordle |
| 38 | +def get_row_results(game_row): |
| 39 | + tiles = game_row.find_elements( |
| 40 | + By.XPATH, ".//*[contains(@class, 'Tile-module_tile__')]") |
| 41 | + row_results = [] |
| 42 | + res_to_int = { |
| 43 | + "correct": 1, |
| 44 | + "present": 0, |
| 45 | + "absent": -1, |
| 46 | + "empty": -2, |
| 47 | + "tbd": -3 |
| 48 | + } |
| 49 | + for tile in tiles: |
| 50 | + row_results.append(res_to_int[tile.get_attribute("data-state")]) |
| 51 | + print(f"Row results : {row_results}") |
| 52 | + |
| 53 | + return tuple(row_results) |
| 54 | + |
| 55 | + |
| 56 | +# Enter the word in the wordle |
| 57 | +def enter_word(word): |
| 58 | + keyboard_controller = keyboard.Controller() |
| 59 | + keyboard_controller.type(word) |
| 60 | + keyboard_controller.tap(keyboard.Key.enter) |
| 61 | + time.sleep(2) |
| 62 | + |
| 63 | + |
| 64 | +# Check word length, used in get_list_of_words() |
| 65 | +# if the source list contains words with different length |
| 66 | +def check_word_length(word): |
| 67 | + if len(word) != 5: |
| 68 | + return False |
| 69 | + else: |
| 70 | + return True |
| 71 | + |
| 72 | + |
| 73 | +# Check if a word contains a specific letter |
| 74 | +def check_letter_in_word(letter, word): |
| 75 | + if letter in word: |
| 76 | + return True |
| 77 | + else: |
| 78 | + return False |
| 79 | + |
| 80 | + |
| 81 | +# Check if the letter in the finder object |
| 82 | +# is the same as the letter in the possible answer |
| 83 | +def check_match(finder_word_letter, possible_word_letter): |
| 84 | + if finder_word_letter == possible_word_letter: |
| 85 | + return True |
| 86 | + else: |
| 87 | + return False |
| 88 | + |
| 89 | + |
| 90 | +# From the wordle words list, return all the words |
| 91 | +def get_list_of_words(): |
| 92 | + list_of_words = open("words_alpha.txt", "r").read().strip().splitlines() |
| 93 | + |
| 94 | + # *** Use this if the source list contains words with different length *** |
| 95 | + # list_of_words = list(filter(check_word_length, list_of_words)) |
| 96 | + |
| 97 | + return list_of_words |
| 98 | + |
| 99 | + |
| 100 | +# Algorithm that solve the wordle |
| 101 | +def solving_algorithm(res, finder): |
| 102 | + print("Starting solving algorithm") |
| 103 | + word = finder.word_to_try |
| 104 | + |
| 105 | + # Compare the word with the results of the wordle |
| 106 | + for letter in range(len(word)): |
| 107 | + # Case when the status of the letter is "correct" |
| 108 | + if res[letter] == 1: |
| 109 | + print(f"Letter {word[letter]} is correct") |
| 110 | + finder.word[letter] = word[letter] |
| 111 | + print(finder.word) |
| 112 | + if word[letter] in finder.absent_letters: |
| 113 | + finder.absent_letters.remove(word[letter]) |
| 114 | + |
| 115 | + # Case when the status of the letter is "present" |
| 116 | + # (present but at the wrong position) |
| 117 | + elif res[letter] == 0: |
| 118 | + print(f"Letter {word[letter]} is present") |
| 119 | + finder.present_letters.add(word[letter]) |
| 120 | + # We keep all the words that don't match |
| 121 | + # the pattern of the word entered |
| 122 | + finder.possible_words = list( |
| 123 | + filter(lambda x_word: |
| 124 | + not check_match(word[letter], x_word[letter]), |
| 125 | + finder.possible_words)) |
| 126 | + |
| 127 | + else: # Case when the status of the letter is "absent" |
| 128 | + print(f"Letter {word[letter]} is absent") |
| 129 | + if word[letter] not in finder.present_letters: |
| 130 | + finder.absent_letters.add(word[letter]) |
| 131 | + |
| 132 | + # We keep all the words that don't match |
| 133 | + # the pattern of the word entered |
| 134 | + finder.possible_words = list( |
| 135 | + filter(lambda x_word: |
| 136 | + not check_match(word[letter], x_word[letter]), |
| 137 | + finder.possible_words)) |
| 138 | + |
| 139 | + print("\n") |
| 140 | + print("Updating list of possible words ...") |
| 141 | + |
| 142 | + # Update list of words |
| 143 | + for absent in finder.absent_letters: |
| 144 | + finder.possible_words = list( |
| 145 | + filter(lambda x_word: |
| 146 | + not check_letter_in_word(absent, x_word), |
| 147 | + finder.possible_words)) |
| 148 | + for present in finder.present_letters: |
| 149 | + finder.possible_words = list( |
| 150 | + filter(lambda x_word: |
| 151 | + check_letter_in_word(present, x_word), |
| 152 | + finder.possible_words)) |
| 153 | + for i in range(len(finder.word)): |
| 154 | + if finder.word[i] != "": |
| 155 | + finder.possible_words = list( |
| 156 | + filter(lambda x_word: |
| 157 | + check_match(x_word[i], finder.word[i]), |
| 158 | + finder.possible_words)) |
| 159 | + |
| 160 | + # Update the next word to try |
| 161 | + finder.word_to_try = finder.possible_words[0] |
| 162 | + |
| 163 | + print("List of possible words updated !\n") |
| 164 | + |
| 165 | + print("Letter not in the right position : ", finder.present_letters) |
| 166 | + print("Letters with absent status : ", finder.absent_letters) |
| 167 | + print("List of words : ", finder.possible_words) |
| 168 | + print("Length of list", len(finder.possible_words)) |
| 169 | + |
| 170 | + |
| 171 | +def main(): |
| 172 | + # Start the browser |
| 173 | + browser = webdriver.Firefox( |
| 174 | + service=FirefoxService(GeckoDriverManager().install())) |
| 175 | + browser.get("https://www.nytimes.com/games/wordle/index.html") |
| 176 | + |
| 177 | + # Create the finder object (cf. class Finder) |
| 178 | + finder = Finder() |
| 179 | + |
| 180 | + guesses_left = 6 |
| 181 | + |
| 182 | + # Wait for start |
| 183 | + with keyboard.Listener(on_release=on_release, suppress=True) as listener: |
| 184 | + print("Starting program\n") |
| 185 | + listener.join() |
| 186 | + |
| 187 | + # With "suppress=True", duplicate key presses are not sent |
| 188 | + # to the application but for some reason I need to add a delay |
| 189 | + # for the first input to be sent. |
| 190 | + time.sleep(1) |
| 191 | + |
| 192 | + # Get the game rows |
| 193 | + game_rows = browser.find_elements( |
| 194 | + By.XPATH, "//*[contains(@class, 'Row-module_row__')]") |
| 195 | + |
| 196 | + # Enter words until the game is over or the wordle is solved |
| 197 | + for i in range(guesses_left, 0, -1): |
| 198 | + enter_word(finder.word_to_try) |
| 199 | + res = get_row_results(game_rows[guesses_left - i]) |
| 200 | + solving_algorithm(res, finder) |
| 201 | + if len(finder.possible_words) == 1: |
| 202 | + enter_word(finder.word_to_try) |
| 203 | + print(f"The word is : {finder.word_to_try}\n") |
| 204 | + break |
| 205 | + time.sleep(1) |
| 206 | + |
| 207 | + |
| 208 | +if __name__ == "__main__": |
| 209 | + main() |
0 commit comments